@oh-my-pi/pi-coding-agent 16.1.7 → 16.1.9

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 (120) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cli.js +4266 -3446
  3. package/dist/types/cli/flag-tables.d.ts +17 -0
  4. package/dist/types/cli/session-picker.d.ts +0 -1
  5. package/dist/types/cli/tiny-models-cli.d.ts +2 -0
  6. package/dist/types/cli-commands.d.ts +4 -2
  7. package/dist/types/collab/protocol.d.ts +20 -1
  8. package/dist/types/config/settings-schema.d.ts +57 -6
  9. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +5 -1
  10. package/dist/types/internal-urls/filesystem-resource.d.ts +9 -0
  11. package/dist/types/mcp/loader.d.ts +3 -2
  12. package/dist/types/mcp/manager.d.ts +4 -3
  13. package/dist/types/mcp/startup-events.d.ts +21 -4
  14. package/dist/types/mnemopi/config.d.ts +1 -0
  15. package/dist/types/modes/components/agent-hub.d.ts +1 -0
  16. package/dist/types/modes/components/agent-transcript-viewer.d.ts +1 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  18. package/dist/types/modes/components/btw-panel.d.ts +2 -0
  19. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -0
  20. package/dist/types/modes/components/session-selector.d.ts +0 -2
  21. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  22. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  23. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  24. package/dist/types/modes/controllers/streaming-reveal.d.ts +3 -2
  25. package/dist/types/modes/interactive-mode.d.ts +4 -1
  26. package/dist/types/modes/types.d.ts +4 -1
  27. package/dist/types/session/agent-session.d.ts +18 -3
  28. package/dist/types/session/session-history-format.d.ts +25 -0
  29. package/dist/types/session/snapcompact-inline.d.ts +1 -1
  30. package/dist/types/slash-commands/builtin-registry.d.ts +7 -4
  31. package/dist/types/slash-commands/types.d.ts +2 -0
  32. package/dist/types/system-prompt.d.ts +1 -0
  33. package/dist/types/tiny/models.d.ts +11 -7
  34. package/dist/types/tools/browser/launch.d.ts +5 -0
  35. package/dist/types/tools/todo.d.ts +1 -0
  36. package/dist/types/utils/thinking-display.d.ts +2 -0
  37. package/package.json +12 -12
  38. package/src/advisor/__tests__/advisor.test.ts +104 -0
  39. package/src/advisor/runtime.ts +38 -2
  40. package/src/cli/args.ts +0 -1
  41. package/src/cli/flag-tables.ts +42 -0
  42. package/src/cli/profile-bootstrap.ts +1 -11
  43. package/src/cli/session-picker.ts +1 -2
  44. package/src/cli/tiny-models-cli.ts +7 -2
  45. package/src/cli-commands.ts +48 -3
  46. package/src/collab/guest.ts +172 -20
  47. package/src/collab/host.ts +47 -5
  48. package/src/collab/protocol.ts +16 -1
  49. package/src/config/model-registry.ts +6 -7
  50. package/src/config/settings-schema.ts +59 -5
  51. package/src/edit/renderer.ts +8 -12
  52. package/src/extensibility/legacy-pi-ai-shim.ts +16 -4
  53. package/src/internal-urls/docs-index.generated.txt +1 -1
  54. package/src/internal-urls/filesystem-resource.ts +34 -0
  55. package/src/internal-urls/local-protocol.ts +7 -1
  56. package/src/internal-urls/memory-protocol.ts +5 -1
  57. package/src/internal-urls/skill-protocol.ts +20 -4
  58. package/src/internal-urls/vault-protocol.ts +5 -2
  59. package/src/lsp/client.ts +39 -25
  60. package/src/main.ts +8 -8
  61. package/src/mcp/loader.ts +4 -3
  62. package/src/mcp/manager.ts +35 -15
  63. package/src/mcp/startup-events.ts +106 -11
  64. package/src/mnemopi/config.ts +2 -0
  65. package/src/mnemopi/state.ts +3 -1
  66. package/src/modes/components/agent-hub.ts +4 -0
  67. package/src/modes/components/agent-transcript-viewer.ts +2 -0
  68. package/src/modes/components/assistant-message.ts +217 -18
  69. package/src/modes/components/btw-panel.ts +15 -3
  70. package/src/modes/components/chat-transcript-builder.ts +8 -2
  71. package/src/modes/components/model-selector.ts +72 -9
  72. package/src/modes/components/omfg-panel.ts +1 -1
  73. package/src/modes/components/session-selector.ts +4 -9
  74. package/src/modes/components/snapcompact-shape-preview.ts +1 -1
  75. package/src/modes/components/tool-execution.ts +10 -2
  76. package/src/modes/controllers/btw-controller.ts +32 -7
  77. package/src/modes/controllers/command-controller.ts +18 -3
  78. package/src/modes/controllers/event-controller.ts +24 -4
  79. package/src/modes/controllers/input-controller.ts +43 -21
  80. package/src/modes/controllers/mcp-command-controller.ts +40 -12
  81. package/src/modes/controllers/selector-controller.ts +23 -13
  82. package/src/modes/controllers/streaming-reveal.ts +78 -20
  83. package/src/modes/controllers/todo-command-controller.ts +9 -10
  84. package/src/modes/interactive-mode.ts +84 -9
  85. package/src/modes/types.ts +4 -1
  86. package/src/modes/utils/ui-helpers.ts +1 -0
  87. package/src/prompts/advisor/system.md +1 -1
  88. package/src/prompts/system/side-channel-no-tools.md +3 -0
  89. package/src/sdk.ts +12 -8
  90. package/src/session/agent-session.ts +385 -85
  91. package/src/session/history-storage.ts +15 -16
  92. package/src/session/session-history-format.ts +41 -1
  93. package/src/session/snapcompact-inline.ts +9 -6
  94. package/src/slash-commands/builtin-registry.ts +171 -87
  95. package/src/slash-commands/helpers/todo.ts +12 -6
  96. package/src/slash-commands/types.ts +2 -0
  97. package/src/system-prompt.ts +6 -11
  98. package/src/tiny/models.ts +7 -3
  99. package/src/tiny/title-client.ts +8 -2
  100. package/src/tiny/worker.ts +1 -0
  101. package/src/tools/browser/launch.ts +107 -31
  102. package/src/tools/puppeteer/00_stealth_tampering.txt +16 -35
  103. package/src/tools/puppeteer/01_stealth_activity.txt +79 -19
  104. package/src/tools/puppeteer/02_stealth_hairline.txt +57 -11
  105. package/src/tools/puppeteer/03_stealth_botd.txt +10 -14
  106. package/src/tools/puppeteer/04_stealth_iframe.txt +156 -63
  107. package/src/tools/puppeteer/05_stealth_webgl.txt +222 -64
  108. package/src/tools/puppeteer/06_stealth_screen.txt +255 -67
  109. package/src/tools/puppeteer/07_stealth_fonts.txt +17 -15
  110. package/src/tools/puppeteer/08_stealth_audio.txt +32 -20
  111. package/src/tools/puppeteer/09_stealth_locale.txt +45 -40
  112. package/src/tools/puppeteer/10_stealth_plugins.txt +12 -8
  113. package/src/tools/puppeteer/11_stealth_hardware.txt +57 -6
  114. package/src/tools/puppeteer/12_stealth_codecs.txt +20 -18
  115. package/src/tools/puppeteer/13_stealth_worker.txt +206 -45
  116. package/src/tools/search.ts +10 -1
  117. package/src/tools/sqlite-reader.ts +59 -3
  118. package/src/tools/todo.ts +6 -0
  119. package/src/tools/write.ts +4 -6
  120. package/src/utils/thinking-display.ts +78 -0
@@ -49,6 +49,7 @@ import {
49
49
  collectEntriesForBranchSummary,
50
50
  collectShakeRegions,
51
51
  compact,
52
+ compactionContextTokens,
52
53
  createCompactionSummaryMessage,
53
54
  DEFAULT_SHAKE_CONFIG,
54
55
  effectiveReserveTokens,
@@ -74,7 +75,6 @@ import {
74
75
  import type { ProtectedToolMatcher } from "@oh-my-pi/pi-agent-core/compaction/tool-protection";
75
76
  import type {
76
77
  AssistantMessage,
77
- Context,
78
78
  ImageContent,
79
79
  Message,
80
80
  MessageAttribution,
@@ -103,8 +103,9 @@ import {
103
103
  resolveServiceTier,
104
104
  streamSimple,
105
105
  } from "@oh-my-pi/pi-ai";
106
- import { stripToolDescriptions } from "@oh-my-pi/pi-ai/utils/schema";
106
+ import { stripToolDescriptions, toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
107
107
  import { THINKING_LOOP_ERROR_MARKER } from "@oh-my-pi/pi-ai/utils/thinking-loop";
108
+ import { isFireworksFastModelId, toFireworksBaseModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id";
108
109
  import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
109
110
  import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
110
111
  import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
@@ -158,7 +159,7 @@ import {
158
159
  import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
159
160
  import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
160
161
  import type { Settings, SkillsSettings } from "../config/settings";
161
- import { onAppendOnlyModeChanged } from "../config/settings";
162
+ import { getDefault, onAppendOnlyModeChanged } from "../config/settings";
162
163
  import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
163
164
  import { loadCapability } from "../discovery";
164
165
  import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
@@ -228,6 +229,7 @@ import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" w
228
229
  import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool-decision-reminder.md" with {
229
230
  type: "text",
230
231
  };
232
+ import sideChannelNoToolsReminder from "../prompts/system/side-channel-no-tools.md" with { type: "text" };
231
233
  import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
232
234
  import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
233
235
  import unexpectedStopRetryTemplate from "../prompts/system/unexpected-stop-retry.md" with { type: "text" };
@@ -311,6 +313,22 @@ import { YieldQueue } from "./yield-queue";
311
313
 
312
314
  const SESSION_STOP_CONTINUATION_CAP = 8;
313
315
 
316
+ // A side-channel assistant response is signed for the hidden prompt/history that
317
+ // produced it. If we persist that response under a different user turn, native
318
+ // replay anchors become invalid; keep only visible, non-cryptographic content.
319
+ function sanitizeAssistantForReparentedHistory(message: AssistantMessage): AssistantMessage {
320
+ const content: AssistantMessage["content"] = [];
321
+ for (const block of message.content) {
322
+ if (block.type === "redactedThinking") continue;
323
+ if (block.type === "thinking") {
324
+ content.push({ type: "thinking", thinking: block.thinking });
325
+ continue;
326
+ }
327
+ content.push(block);
328
+ }
329
+ return { ...message, content, providerPayload: undefined };
330
+ }
331
+
314
332
  /** Session-specific events that extend the core AgentEvent */
315
333
  export type AgentSessionEvent =
316
334
  | AgentEvent
@@ -1251,6 +1269,7 @@ export class AgentSession {
1251
1269
  #disconnectOwnedMcpManager: (() => Promise<void>) | undefined;
1252
1270
  #requestedToolNames: ReadonlySet<string> | undefined;
1253
1271
  #baseSystemPrompt: string[];
1272
+ #baseSystemPromptBeforeMemoryPromotion: string[] | undefined;
1254
1273
  /**
1255
1274
  * Signature of the (toolNames, tool descriptions) tuple passed to the most
1256
1275
  * recent successful `rebuildSystemPrompt` call. Used to skip redundant rebuilds
@@ -1337,6 +1356,7 @@ export class AgentSession {
1337
1356
  #pruneToolDescriptions = false;
1338
1357
  #checkpointState: CheckpointState | undefined = undefined;
1339
1358
  #pendingRewindReport: string | undefined = undefined;
1359
+ #rewoundToolResultIds = new Set<string>();
1340
1360
  #lastSuccessfulYieldToolCallId: string | undefined = undefined;
1341
1361
  #providerSessionState = new Map<string, ProviderSessionState>();
1342
1362
  #hindsightSessionState: HindsightSessionState | undefined = undefined;
@@ -1537,6 +1557,10 @@ export class AgentSession {
1537
1557
  this.#customCommands = config.customCommands ?? [];
1538
1558
  this.#skillsSettings = config.skillsSettings;
1539
1559
  this.#modelRegistry = config.modelRegistry;
1560
+ // Resolve the wire service-tier per request so the Fireworks Priority
1561
+ // toggle scopes priority to Fireworks alone, without mutating the shared
1562
+ // session `serviceTier` that drives `/fast` and OpenAI/Anthropic priority.
1563
+ this.agent.serviceTierResolver = model => this.#effectiveServiceTier(model);
1540
1564
  this.#advisorReadOnlyTools = config.advisorReadOnlyTools;
1541
1565
  this.#advisorWatchdogPrompt = config.advisorWatchdogPrompt;
1542
1566
  this.#pruneToolDescriptions = config.pruneToolDescriptions === true;
@@ -1574,6 +1598,11 @@ export class AgentSession {
1574
1598
  this.agent.setRawSseEventInterceptor(this.#onSseEvent);
1575
1599
  this.agent.setOnTurnEnd(async (messages, signal) => {
1576
1600
  if (signal?.aborted) return;
1601
+ const rewindReport = this.#extractRewindReport(messages);
1602
+ if (rewindReport) {
1603
+ this.#pendingRewindReport = undefined;
1604
+ await this.#applyRewind(rewindReport, messages);
1605
+ }
1577
1606
  this.#advisorPrimaryTurnsCompleted++;
1578
1607
  if (this.#advisorRuntime && !this.#advisorRuntime.disposed) {
1579
1608
  this.#advisorRuntime.onTurnEnd(messages);
@@ -2516,11 +2545,6 @@ export class AgentSession {
2516
2545
  if (event.type === "tool_execution_end" && event.toolName === "yield" && !event.isError) {
2517
2546
  this.#lastSuccessfulYieldToolCallId = event.toolCallId;
2518
2547
  }
2519
- if (event.type === "turn_end" && this.#pendingRewindReport) {
2520
- const report = this.#pendingRewindReport;
2521
- this.#pendingRewindReport = undefined;
2522
- await this.#applyRewind(report);
2523
- }
2524
2548
 
2525
2549
  // TTSR: Check for pattern matches on assistant text/thinking and tool argument deltas
2526
2550
  if (event.type === "message_update" && this.#ttsrManager?.hasRules()) {
@@ -2603,7 +2627,13 @@ export class AgentSession {
2603
2627
  };
2604
2628
  }
2605
2629
  }
2606
- this.sessionManager.appendMessage(event.message);
2630
+ const skipPersistedRewindResult =
2631
+ event.message.role === "toolResult" &&
2632
+ event.message.toolName === "rewind" &&
2633
+ this.#rewoundToolResultIds.delete(event.message.toolCallId);
2634
+ if (!skipPersistedRewindResult) {
2635
+ this.sessionManager.appendMessage(event.message);
2636
+ }
2607
2637
  }
2608
2638
  // Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
2609
2639
 
@@ -2719,8 +2749,9 @@ export class AgentSession {
2719
2749
 
2720
2750
  // Check auto-retry and auto-compaction after agent completes
2721
2751
  if (event.type === "agent_end") {
2752
+ const settledMessages = this.agent.state.messages;
2722
2753
  const emitAgentEndNotification = async () => {
2723
- await this.#emitAgentEndNotification(event.messages);
2754
+ await this.#emitAgentEndNotification(settledMessages);
2724
2755
  };
2725
2756
  const usage = this.getSessionStats().tokens;
2726
2757
  await this.#goalRuntime.onAgentEnd({
@@ -2731,7 +2762,7 @@ export class AgentSession {
2731
2762
  cacheWrite: usage.cacheWrite,
2732
2763
  },
2733
2764
  });
2734
- const fallbackAssistant = [...event.messages]
2765
+ const fallbackAssistant = [...settledMessages]
2735
2766
  .reverse()
2736
2767
  .find((message): message is AssistantMessage => message.role === "assistant");
2737
2768
  const msg = this.#lastAssistantMessage ?? fallbackAssistant;
@@ -2761,6 +2792,11 @@ export class AgentSession {
2761
2792
 
2762
2793
  if (this.#assistantEndedWithSuccessfulYield(msg)) {
2763
2794
  this.#lastSuccessfulYieldToolCallId = undefined;
2795
+ if (this.#goalModeState?.enabled && this.#goalModeState.goal.status === "active") {
2796
+ const compactionTask = this.#checkCompaction(msg);
2797
+ this.#trackPostPromptTask(compactionTask);
2798
+ await compactionTask;
2799
+ }
2764
2800
  await emitAgentEndNotification();
2765
2801
  return;
2766
2802
  }
@@ -2790,6 +2826,16 @@ export class AgentSession {
2790
2826
  await emitAgentEndNotification();
2791
2827
  return;
2792
2828
  }
2829
+ // Fireworks Fast variants degrade to their base model on a failed turn —
2830
+ // including hard router errors the generic retry classifier rejects — so
2831
+ // run this gate before the standard retryability check.
2832
+ if (this.#isFireworksFastFallbackEligible(msg)) {
2833
+ const didRetry = await this.#handleRetryableError(msg, { fireworksFastFallback: true });
2834
+ if (didRetry) {
2835
+ await emitAgentEndNotification();
2836
+ return;
2837
+ }
2838
+ }
2793
2839
  // Check for retryable errors first (overloaded, rate limit, server errors)
2794
2840
  if (this.#isRetryableError(msg)) {
2795
2841
  const didRetry = await this.#handleRetryableError(msg);
@@ -2828,7 +2874,7 @@ export class AgentSession {
2828
2874
  return;
2829
2875
  }
2830
2876
  }
2831
- await this.#emitSessionStopEvent(event.messages);
2877
+ await this.#emitSessionStopEvent(settledMessages);
2832
2878
  await emitAgentEndNotification();
2833
2879
  }
2834
2880
  };
@@ -4084,18 +4130,34 @@ export class AgentSession {
4084
4130
  }
4085
4131
 
4086
4132
  /** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
4087
- #resetHindsightConversationTrackingIfHindsight(): void {
4088
- if (this.settings.get("memory.backend") !== "hindsight") return;
4133
+ #resetHindsightConversationTrackingIfHindsight(): boolean {
4134
+ if (this.settings.get("memory.backend") !== "hindsight") return false;
4089
4135
  const state = this.getHindsightSessionState();
4090
- if (!state || state.aliasOf) return;
4136
+ if (!state || state.aliasOf) return false;
4091
4137
  state.resetConversationTracking();
4138
+ return true;
4092
4139
  }
4093
4140
 
4094
- #resetMnemopiConversationTrackingIfMnemopi(): void {
4095
- if (this.settings.get("memory.backend") !== "mnemopi") return;
4141
+ #resetMnemopiConversationTrackingIfMnemopi(): boolean {
4142
+ if (this.settings.get("memory.backend") !== "mnemopi") return false;
4096
4143
  const state = this.getMnemopiSessionState();
4097
- if (!state || state.aliasOf) return;
4144
+ if (!state || state.aliasOf) return false;
4098
4145
  state.resetConversationTracking();
4146
+ return true;
4147
+ }
4148
+
4149
+ async #resetMemoryContextForNewTranscript(): Promise<void> {
4150
+ const hadPromotedMemoryPrompt = this.#baseSystemPromptBeforeMemoryPromotion !== undefined;
4151
+ const resetHindsight = this.#resetHindsightConversationTrackingIfHindsight();
4152
+ const resetMnemopi = this.#resetMnemopiConversationTrackingIfMnemopi();
4153
+ if (hadPromotedMemoryPrompt) {
4154
+ this.#baseSystemPrompt = this.#baseSystemPromptBeforeMemoryPromotion!;
4155
+ this.agent.setSystemPrompt(this.#baseSystemPrompt);
4156
+ this.#baseSystemPromptBeforeMemoryPromotion = undefined;
4157
+ }
4158
+ if (resetHindsight || resetMnemopi || hadPromotedMemoryPrompt) {
4159
+ await this.refreshBaseSystemPrompt();
4160
+ }
4099
4161
  }
4100
4162
 
4101
4163
  /** True once dispose() has begun; deferred background work (e.g. the deferred
@@ -4783,6 +4845,7 @@ export class AgentSession {
4783
4845
  if (signature !== this.#lastAppliedToolSignature) {
4784
4846
  const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
4785
4847
  this.#baseSystemPrompt = built.systemPrompt;
4848
+ this.#baseSystemPromptBeforeMemoryPromotion = undefined;
4786
4849
  this.agent.setSystemPrompt(this.#baseSystemPrompt);
4787
4850
  this.#lastAppliedToolSignature = signature;
4788
4851
  this.#promptModelKey = this.#currentPromptModelKey();
@@ -4867,6 +4930,7 @@ export class AgentSession {
4867
4930
  const activeToolNames = this.getActiveToolNames();
4868
4931
  const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
4869
4932
  this.#baseSystemPrompt = built.systemPrompt;
4933
+ this.#baseSystemPromptBeforeMemoryPromotion = undefined;
4870
4934
  this.agent.setSystemPrompt(this.#baseSystemPrompt);
4871
4935
  this.#promptModelKey = this.#currentPromptModelKey();
4872
4936
  // Refresh the cached signature so a subsequent `#applyActiveToolsByName` with
@@ -4885,7 +4949,29 @@ export class AgentSession {
4885
4949
  try {
4886
4950
  const injected = await backend.beforeAgentStartPrompt(this, promptText);
4887
4951
  if (!injected) return this.#baseSystemPrompt;
4888
- return [...this.#baseSystemPrompt, injected];
4952
+
4953
+ const previousBaseSystemPrompt = this.#baseSystemPrompt;
4954
+ try {
4955
+ await this.refreshBaseSystemPrompt();
4956
+ } catch (refreshErr) {
4957
+ logger.debug("Memory backend prompt refresh after beforeAgentStartPrompt failed", {
4958
+ backend: backend.id,
4959
+ error: String(refreshErr),
4960
+ });
4961
+ }
4962
+
4963
+ if (
4964
+ this.#baseSystemPrompt.length !== previousBaseSystemPrompt.length ||
4965
+ this.#baseSystemPrompt.some((part, index) => part !== previousBaseSystemPrompt[index])
4966
+ ) {
4967
+ return this.#baseSystemPrompt;
4968
+ }
4969
+
4970
+ this.#baseSystemPromptBeforeMemoryPromotion ??= previousBaseSystemPrompt;
4971
+ const stablePrompt = [...previousBaseSystemPrompt, injected];
4972
+ this.#baseSystemPrompt = stablePrompt;
4973
+ this.agent.setSystemPrompt(stablePrompt);
4974
+ return stablePrompt;
4889
4975
  } catch (err) {
4890
4976
  logger.debug("Memory backend beforeAgentStartPrompt failed", {
4891
4977
  backend: backend.id,
@@ -6745,8 +6831,7 @@ export class AgentSession {
6745
6831
  this.#syncAgentSessionId();
6746
6832
  this.#rekeyHindsightMemoryForCurrentSessionId();
6747
6833
  this.#rekeyMnemopiMemoryForCurrentSessionId();
6748
- this.#resetHindsightConversationTrackingIfHindsight();
6749
- this.#resetMnemopiConversationTrackingIfMnemopi();
6834
+ await this.#resetMemoryContextForNewTranscript();
6750
6835
  this.#pendingNextTurnMessages = [];
6751
6836
  this.#scheduledHiddenNextTurnGeneration = undefined;
6752
6837
 
@@ -6843,7 +6928,7 @@ export class AgentSession {
6843
6928
  this.#syncAgentSessionId();
6844
6929
  this.#rekeyHindsightMemoryForCurrentSessionId();
6845
6930
  this.#rekeyMnemopiMemoryForCurrentSessionId();
6846
- this.#resetMnemopiConversationTrackingIfMnemopi();
6931
+ await this.#resetMemoryContextForNewTranscript();
6847
6932
 
6848
6933
  // Emit session_switch event with reason "fork" to hooks
6849
6934
  if (this.#extensionRunner) {
@@ -7279,7 +7364,25 @@ export class AgentSession {
7279
7364
  * no model is selected.
7280
7365
  */
7281
7366
  isFastModeActive(): boolean {
7282
- return resolveServiceTier(this.serviceTier, this.model?.provider) === "priority";
7367
+ return resolveServiceTier(this.#effectiveServiceTier(), this.model?.provider) === "priority";
7368
+ }
7369
+
7370
+ /**
7371
+ * Effective wire service-tier for a request to `model`. Fireworks models
7372
+ * take the Priority serving path only when the Providers › Fireworks Tier
7373
+ * setting is `"priority"` — that toggle is the sole opt-in, so a global
7374
+ * `serviceTier: "priority"` (for OpenAI/Anthropic) never silently incurs
7375
+ * Fireworks priority costs — and never for `-fast` variants, whose Fast
7376
+ * serving path is mutually exclusive with Priority. Every other provider
7377
+ * uses the session `serviceTier` unchanged.
7378
+ */
7379
+ #effectiveServiceTier(model: Model | undefined = this.model): ServiceTier | undefined {
7380
+ if (model?.provider === "fireworks") {
7381
+ return this.settings.get("providers.fireworksTier") === "priority" && !isFireworksFastModelId(model.id)
7382
+ ? "priority"
7383
+ : undefined;
7384
+ }
7385
+ return this.serviceTier;
7283
7386
  }
7284
7387
 
7285
7388
  setServiceTier(serviceTier: ServiceTier | undefined): void {
@@ -7991,8 +8094,7 @@ export class AgentSession {
7991
8094
  this.#syncAgentSessionId();
7992
8095
  this.#rekeyHindsightMemoryForCurrentSessionId();
7993
8096
  this.#rekeyMnemopiMemoryForCurrentSessionId();
7994
- this.#resetHindsightConversationTrackingIfHindsight();
7995
- this.#resetMnemopiConversationTrackingIfMnemopi();
8097
+ await this.#resetMemoryContextForNewTranscript();
7996
8098
  this.#pendingNextTurnMessages = [];
7997
8099
  this.#scheduledHiddenNextTurnGeneration = undefined;
7998
8100
  this.#todoReminderCount = 0;
@@ -8039,14 +8141,39 @@ export class AgentSession {
8039
8141
  }
8040
8142
  }
8041
8143
 
8042
- #estimatePrePromptContextTokens(messages: AgentMessage[], contextWindow: number): number {
8043
- const breakdown = this.getContextBreakdown({ contextWindow, pendingMessages: messages });
8144
+ /**
8145
+ * Local token estimate of the stored conversation (plus any pending messages),
8146
+ * independent of provider-reported usage. A `before_provider_request` hook
8147
+ * (e.g. a compression extension such as Headroom) or other on-wire payload
8148
+ * transform can shrink the request below the real stored conversation; the
8149
+ * provider then reports deflated prompt tokens, so anchoring the compaction
8150
+ * decision purely on that usage lets the real history grow unbounded until it
8151
+ * overflows and native compaction can no longer run. This estimate is the
8152
+ * floor the compaction decision respects so on-wire compression can never
8153
+ * suppress it.
8154
+ */
8155
+ #estimateStoredContextTokens(pendingMessages: AgentMessage[] = []): number {
8156
+ // Exclude encrypted reasoning (thinkingSignature / redactedThinking): its
8157
+ // local byte size diverges from what the provider bills, so counting it here
8158
+ // would let a thinking-heavy turn falsely trip the floor. The provider usage
8159
+ // (the other arm of compactionContextTokens) already accounts for it.
8160
+ const opts = { excludeEncryptedReasoning: true } as const;
8044
8161
  return (
8045
- breakdown?.usedTokens ??
8046
- computeNonMessageTokens(this) + messages.reduce((sum, msg) => sum + estimateTokens(msg), 0)
8162
+ computeNonMessageTokens(this) +
8163
+ this.messages.reduce((sum, msg) => sum + estimateTokens(msg, opts), 0) +
8164
+ pendingMessages.reduce((sum, msg) => sum + estimateTokens(msg, opts), 0)
8047
8165
  );
8048
8166
  }
8049
8167
 
8168
+ #estimatePrePromptContextTokens(messages: AgentMessage[], contextWindow: number): number {
8169
+ const breakdown = this.getContextBreakdown({ contextWindow, pendingMessages: messages });
8170
+ const localEstimate = this.#estimateStoredContextTokens(messages);
8171
+ // Floor by the local estimate: a payload-shrinking before_provider_request
8172
+ // hook deflates the provider-anchored breakdown, which must not suppress
8173
+ // pre-prompt compaction (see #estimateStoredContextTokens).
8174
+ return compactionContextTokens(breakdown?.usedTokens ?? 0, localEstimate);
8175
+ }
8176
+
8050
8177
  async #runPrePromptCompactionIfNeeded(messages: AgentMessage[]): Promise<void> {
8051
8178
  const model = this.model;
8052
8179
  if (!model) return;
@@ -8074,7 +8201,10 @@ export class AgentSession {
8074
8201
  contextWindow,
8075
8202
  model: `${model.provider}/${model.id}`,
8076
8203
  });
8077
- await this.#runAutoCompaction("threshold", false, false, false, { autoContinue: false });
8204
+ await this.#runAutoCompaction("threshold", false, false, false, {
8205
+ autoContinue: false,
8206
+ triggerContextTokens: contextTokens,
8207
+ });
8078
8208
  }
8079
8209
 
8080
8210
  /**
@@ -8209,6 +8339,12 @@ export class AgentSession {
8209
8339
  if (pruneResult) {
8210
8340
  contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
8211
8341
  }
8342
+ // Floor by the real stored-conversation estimate so a payload-shrinking
8343
+ // before_provider_request hook (e.g. a compression extension such as
8344
+ // Headroom) can't deflate the provider-reported usage below the true
8345
+ // history size and skip the threshold. The estimate runs after the prune
8346
+ // passes above, so it reflects the post-prune message set.
8347
+ contextTokens = compactionContextTokens(contextTokens, this.#estimateStoredContextTokens());
8212
8348
  if (shouldCompact(contextTokens, contextWindow, compactionSettings)) {
8213
8349
  // Try promotion first — if a larger model is available, switch instead of compacting
8214
8350
  const promoted = await this.#tryContextPromotion(assistantMessage);
@@ -8427,14 +8563,29 @@ export class AgentSession {
8427
8563
  return true;
8428
8564
  }
8429
8565
 
8430
- async #applyRewind(report: string): Promise<void> {
8566
+ #extractRewindReport(messages: AgentMessage[]): string | undefined {
8567
+ if (!this.#checkpointState) return undefined;
8568
+ if (this.#pendingRewindReport) return this.#pendingRewindReport;
8569
+ for (let i = messages.length - 1; i >= 0; i--) {
8570
+ const message = messages[i];
8571
+ if (message?.role !== "toolResult" || message.toolName !== "rewind" || message.isError) continue;
8572
+ const details = message.details;
8573
+ const detailReport =
8574
+ details && typeof details === "object" && "report" in details && typeof details.report === "string"
8575
+ ? details.report.trim()
8576
+ : "";
8577
+ const textReport = message.content.find(part => part.type === "text")?.text.trim() ?? "";
8578
+ const report = detailReport || textReport;
8579
+ return report.length > 0 ? report : undefined;
8580
+ }
8581
+ return undefined;
8582
+ }
8583
+
8584
+ async #applyRewind(report: string, activeMessages?: AgentMessage[]): Promise<void> {
8431
8585
  const checkpointState = this.#checkpointState;
8432
8586
  if (!checkpointState) {
8433
8587
  return;
8434
8588
  }
8435
- const safeCount = Math.max(0, Math.min(checkpointState.checkpointMessageCount, this.agent.state.messages.length));
8436
- this.agent.replaceMessages(this.agent.state.messages.slice(0, safeCount));
8437
- this.#advisorRuntime?.reset();
8438
8589
  try {
8439
8590
  this.sessionManager.branchWithSummary(checkpointState.checkpointEntryId, report, {
8440
8591
  startedAt: checkpointState.startedAt,
@@ -8446,16 +8597,24 @@ export class AgentSession {
8446
8597
  this.sessionManager.branchWithSummary(null, report, { startedAt: checkpointState.startedAt });
8447
8598
  }
8448
8599
  const details = { startedAt: checkpointState.startedAt, rewoundAt: new Date().toISOString() };
8449
- this.agent.appendMessage({
8450
- role: "custom",
8451
- customType: "rewind-report",
8452
- content: report,
8453
- display: false,
8454
- details,
8455
- attribution: "agent",
8456
- timestamp: Date.now(),
8457
- });
8458
8600
  this.sessionManager.appendCustomMessageEntry("rewind-report", report, false, details, "agent");
8601
+
8602
+ if (activeMessages) {
8603
+ for (const message of activeMessages) {
8604
+ if (message.role === "toolResult" && message.toolName === "rewind") {
8605
+ this.#rewoundToolResultIds.add(message.toolCallId);
8606
+ }
8607
+ }
8608
+ }
8609
+ const sessionContext = this.buildDisplaySessionContext();
8610
+ if (activeMessages) {
8611
+ activeMessages.splice(0, activeMessages.length, ...sessionContext.messages);
8612
+ }
8613
+ await this.#restoreMCPSelectionsForSessionContext(sessionContext);
8614
+ this.agent.replaceMessages(activeMessages ?? sessionContext.messages);
8615
+ this.#resetAdvisorSessionState();
8616
+ this.#syncTodoPhasesFromBranch();
8617
+ this.#closeCodexProviderSessionsForHistoryRewrite();
8459
8618
  this.#checkpointState = undefined;
8460
8619
  this.#pendingRewindReport = undefined;
8461
8620
  }
@@ -9495,29 +9654,50 @@ export class AgentSession {
9495
9654
  // summarizer path.
9496
9655
  let snapcompactResult: snapcompact.CompactionResult | undefined;
9497
9656
  if (action === "snapcompact" && compactionPrep.kind !== "fromHook") {
9498
- snapcompactResult = await snapcompact.compact(preparation, {
9499
- convertToLlm,
9500
- model: this.model,
9501
- });
9502
- const ctxWindow = this.model?.contextWindow ?? 0;
9503
- const budget =
9504
- ctxWindow > 0
9505
- ? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
9506
- : Number.POSITIVE_INFINITY;
9507
- const projected = this.#projectSnapcompactContextTokens(preparation, snapcompactResult);
9508
- if (projected > budget) {
9509
- logger.warn("Snapcompact still overflows the window; falling back to an LLM summary", {
9657
+ const text = snapcompact.serializeConversation(
9658
+ convertToLlm(preparation.messagesToSummarize.concat(preparation.turnPrefixMessages)),
9659
+ );
9660
+ const renderScan = snapcompact.scanRenderability(text);
9661
+ if (renderScan.isSafe) {
9662
+ snapcompactResult = await snapcompact.compact(preparation, {
9663
+ convertToLlm,
9664
+ model: this.model,
9665
+ shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
9666
+ });
9667
+ } else {
9668
+ logger.warn("Snapcompact disabled: high non-ASCII rate detected; falling back to an LLM summary", {
9510
9669
  model: this.model?.id,
9511
- projected,
9512
- budget,
9670
+ unrenderableRatio: renderScan.unrenderableRatio,
9513
9671
  });
9514
9672
  this.emitNotice(
9515
9673
  "warning",
9516
- "snapcompact could not bring the context under the limit using an LLM summary instead",
9674
+ `snapcompact disabled: high non-ASCII rate detected (${(renderScan.unrenderableRatio * 100).toFixed(1)}%). Falling back to an LLM summary to prevent data loss.`,
9517
9675
  "compaction",
9518
9676
  );
9519
9677
  action = "context-full";
9520
- snapcompactResult = undefined;
9678
+ }
9679
+
9680
+ if (snapcompactResult) {
9681
+ const ctxWindow = this.model?.contextWindow ?? 0;
9682
+ const budget =
9683
+ ctxWindow > 0
9684
+ ? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
9685
+ : Number.POSITIVE_INFINITY;
9686
+ const projected = this.#projectSnapcompactContextTokens(preparation, snapcompactResult);
9687
+ if (projected > budget) {
9688
+ logger.warn("Snapcompact still overflows the window; falling back to an LLM summary", {
9689
+ model: this.model?.id,
9690
+ projected,
9691
+ budget,
9692
+ });
9693
+ this.emitNotice(
9694
+ "warning",
9695
+ "snapcompact could not bring the context under the limit — using an LLM summary instead",
9696
+ "compaction",
9697
+ );
9698
+ action = "context-full";
9699
+ snapcompactResult = undefined;
9700
+ }
9521
9701
  }
9522
9702
  }
9523
9703
 
@@ -9922,7 +10102,8 @@ export class AgentSession {
9922
10102
  result: undefined,
9923
10103
  aborted: false,
9924
10104
  willRetry: false,
9925
- errorMessage: `Auto-shake failed: ${message}`,
10105
+ errorMessage: message,
10106
+ skipped: false,
9926
10107
  });
9927
10108
  // Overflow still needs recovery even if shake threw.
9928
10109
  return reason === "overflow" ? "fallback" : COMPACTION_CHECK_NONE;
@@ -9939,7 +10120,8 @@ export class AgentSession {
9939
10120
  setAutoCompactionEnabled(enabled: boolean): void {
9940
10121
  this.settings.set("compaction.enabled", enabled);
9941
10122
  if (enabled && this.settings.get("compaction.strategy") === "off") {
9942
- this.settings.set("compaction.strategy", "context-full");
10123
+ const defaultStrategy = getDefault("compaction.strategy");
10124
+ this.settings.set("compaction.strategy", defaultStrategy === "off" ? "context-full" : defaultStrategy);
9943
10125
  }
9944
10126
  }
9945
10127
 
@@ -10277,6 +10459,68 @@ export class AgentSession {
10277
10459
  return false;
10278
10460
  }
10279
10461
 
10462
+ /** The active model when it is a Fireworks Fast (`-fast`) variant, else undefined. */
10463
+ #activeFireworksFastModel(): Model | undefined {
10464
+ const model = this.model;
10465
+ return model?.provider === "fireworks" && isFireworksFastModelId(model.id) ? model : undefined;
10466
+ }
10467
+
10468
+ /**
10469
+ * True when the current turn failed on a Fireworks Fast (`-fast`) model in a
10470
+ * way that should degrade to the reliable base (Standard) model. Fast is a
10471
+ * speed-optimized router with no SLA, so any *pre-content* failure — a
10472
+ * transient overload/5xx or a hard "router/model not found / unsupported" —
10473
+ * is worth retrying on the base id. Skips failures the base model shares:
10474
+ * context overflow (compaction's job), usage limits and auth errors (same
10475
+ * account/key), and turns that already emitted a tool call (replaying would
10476
+ * duplicate work). Requires the base model to exist in the registry.
10477
+ */
10478
+ #isFireworksFastFallbackEligible(message: AssistantMessage): boolean {
10479
+ const model = this.#activeFireworksFastModel();
10480
+ if (!model) return false;
10481
+ if (message.stopReason !== "error" || !message.errorMessage) return false;
10482
+ if (message.content.some(block => block.type === "toolCall")) return false;
10483
+ // A content refusal/sensitivity stop is the model's decision, not a route
10484
+ // failure — switching to the base model would just re-trigger it.
10485
+ if (this.#isClassifierRefusal(message)) return false;
10486
+ if (isContextOverflow(message, model.contextWindow ?? 0)) return false;
10487
+ const err = message.errorMessage;
10488
+ if (isUsageLimitError(err)) return false;
10489
+ if (
10490
+ /\b(?:401|403|unauthorized|forbidden|authentication|auth[_ ]?unavailable|no auth available|(?:invalid|no)[_ ]?api[_ ]?key)\b/i.test(
10491
+ err,
10492
+ )
10493
+ )
10494
+ return false;
10495
+ return this.#modelRegistry.find("fireworks", toFireworksBaseModelId(model.id)) !== undefined;
10496
+ }
10497
+
10498
+ /**
10499
+ * Switch the active model from a Fireworks Fast (`-fast`) variant to its base
10500
+ * (Standard) id and stick there for the rest of the session — the auto
10501
+ * fallback that makes Fast a safe default. Returns false when the current
10502
+ * model is not a fast variant, the base id is missing, or it has no key.
10503
+ */
10504
+ async #tryFireworksFastFallback(currentSelector: string): Promise<boolean> {
10505
+ const model = this.#activeFireworksFastModel();
10506
+ if (!model) return false;
10507
+ const baseModel = this.#modelRegistry.find("fireworks", toFireworksBaseModelId(model.id));
10508
+ if (!baseModel) return false;
10509
+ const apiKey = await this.#modelRegistry.getApiKey(baseModel, this.sessionId);
10510
+ if (!apiKey) return false;
10511
+ const baseSelector = formatModelStringWithRouting(baseModel);
10512
+ this.#setModelWithProviderSessionReset(baseModel);
10513
+ this.sessionManager.appendModelChange(baseSelector, EPHEMERAL_MODEL_CHANGE_ROLE);
10514
+ this.settings.getStorage()?.recordModelUsage(baseSelector);
10515
+ await this.#emitSessionEvent({
10516
+ type: "retry_fallback_applied",
10517
+ from: currentSelector,
10518
+ to: baseSelector,
10519
+ role: "fireworks-fast",
10520
+ });
10521
+ return true;
10522
+ }
10523
+
10280
10524
  async #maybeRestoreRetryFallbackPrimary(): Promise<void> {
10281
10525
  if (!this.#activeRetryFallback) return;
10282
10526
  if (this.#activeRetryFallback.pinned) return;
@@ -10379,10 +10623,13 @@ export class AgentSession {
10379
10623
  */
10380
10624
  async #handleRetryableError(
10381
10625
  message: AssistantMessage,
10382
- options?: { allowModelFallback?: boolean },
10626
+ options?: { allowModelFallback?: boolean; fireworksFastFallback?: boolean },
10383
10627
  ): Promise<boolean> {
10384
10628
  const retrySettings = this.settings.getGroup("retry");
10385
- if (!retrySettings.enabled) return false;
10629
+ // The Fireworks Fast→base degrade is an intrinsic model-selection safety net,
10630
+ // not a retry loop, so it runs even when the user disabled retries: it switches
10631
+ // the model once and lets the base turn proceed.
10632
+ if (!retrySettings.enabled && !options?.fireworksFastFallback) return false;
10386
10633
  const classifierRefusal = this.#isClassifierRefusal(message);
10387
10634
 
10388
10635
  const generation = this.#promptGeneration;
@@ -10477,6 +10724,13 @@ export class AgentSession {
10477
10724
  }
10478
10725
  switchedModel = await this.#tryRetryModelFallback(currentSelector, { pinFallback: classifierRefusal });
10479
10726
  }
10727
+ // Auto fallback from a Fireworks Fast variant to its base model. Independent
10728
+ // of the role-fallback setting: it's intrinsic to the Fast contract (speed
10729
+ // best-effort, degrade to Standard on failure) and triggers on hard router
10730
+ // errors the generic retry classifier would otherwise reject.
10731
+ if (!switchedModel && allowModelFallback && options?.fireworksFastFallback) {
10732
+ switchedModel = await this.#tryFireworksFastFallback(currentSelector);
10733
+ }
10480
10734
  if (switchedModel) {
10481
10735
  delayMs = 0;
10482
10736
  } else if (usageLimitWaitMs === undefined && parsedRetryAfterMs && parsedRetryAfterMs > delayMs) {
@@ -10488,6 +10742,15 @@ export class AgentSession {
10488
10742
  this.#resolveRetry();
10489
10743
  return false;
10490
10744
  }
10745
+ // Fast→base was requested but the base switch could not happen (e.g. the
10746
+ // base model has no credential). Don't fall through to backing-off and
10747
+ // retrying the failing fast model for a hard router error that the generic
10748
+ // classifier wouldn't retry — surface it instead.
10749
+ if (options?.fireworksFastFallback && !switchedModel && !this.#isRetryableError(message)) {
10750
+ this.#retryAttempt = 0;
10751
+ this.#resolveRetry();
10752
+ return false;
10753
+ }
10491
10754
 
10492
10755
  // Fail-fast cap: if the provider asks us to wait longer than
10493
10756
  // retry.maxDelayMs and we have no fallback credential or model to
@@ -11040,12 +11303,14 @@ export class AgentSession {
11040
11303
 
11041
11304
  /**
11042
11305
  * Run a single ephemeral side-channel turn against this session's current
11043
- * model + system prompt + history. No tools are used; the side request
11044
- * does not block on, or interfere with, any in-flight main turn. The
11306
+ * model + system prompt + history. The main turn's tool catalog is sent
11307
+ * to preserve the prompt cache, but the model is reminded not to call
11308
+ * tools and any tool calls are discarded. The side request
11309
+ * does not block on, or interfere with, any in-flight main turn. The
11045
11310
  * session's history and persisted state are NOT modified by this call.
11046
11311
  *
11047
11312
  * Used by `BtwController` (`/btw`) and `OmfgController` (`/omfg`) to share
11048
- * the snapshot + stream pipeline. The snapshot includes any in-flight
11313
+ * the snapshot + stream pipeline. The snapshot includes any in-flight
11049
11314
  * streaming assistant text so the model sees the half-finished response
11050
11315
  * rather than missing context.
11051
11316
  */
@@ -11062,15 +11327,7 @@ export class AgentSession {
11062
11327
  const cacheSessionId = this.sessionId;
11063
11328
  const snapshot = this.#buildEphemeralSnapshot(args.promptText);
11064
11329
  const llmMessages = await this.convertMessagesToLlm(snapshot, args.signal);
11065
- const context: Context = {
11066
- systemPrompt: this.systemPrompt,
11067
- messages: llmMessages,
11068
- // Empty tools array: with toolChoice="none" some encoders still serialize the
11069
- // recipient's tool catalog and the model leaks raw call markup
11070
- // (<function_calls>, DSML envelopes) into IRC replies. Stripping tools here
11071
- // removes the surface entirely.
11072
- tools: [],
11073
- };
11330
+ const context = await this.agent.buildSideRequestContext(llmMessages);
11074
11331
  const options = this.prepareSimpleStreamOptions(
11075
11332
  {
11076
11333
  apiKey: this.#modelRegistry.resolver(model, cacheSessionId),
@@ -11084,9 +11341,8 @@ export class AgentSession {
11084
11341
  reasoning: toReasoningEffort(this.thinkingLevel),
11085
11342
  disableReasoning: shouldDisableReasoning(this.thinkingLevel),
11086
11343
  hideThinkingSummary: this.agent.hideThinkingSummary,
11087
- serviceTier: this.serviceTier,
11344
+ serviceTier: this.#effectiveServiceTier(model),
11088
11345
  signal: args.signal,
11089
- toolChoice: "none",
11090
11346
  },
11091
11347
  model.provider,
11092
11348
  );
@@ -11126,9 +11382,13 @@ export class AgentSession {
11126
11382
  if (args.onTextDelta && replyText.length > emittedReplyText.length) {
11127
11383
  args.onTextDelta(replyText.slice(emittedReplyText.length));
11128
11384
  }
11385
+ const sanitizedMessage: AssistantMessage = {
11386
+ ...assistantMessage,
11387
+ content: assistantMessage.content.filter(block => block.type !== "toolCall"),
11388
+ };
11129
11389
  return {
11130
11390
  replyText: args.dedupeReply === false ? replyText.trim() : dedupeEphemeralReply(replyText.trim()),
11131
- assistantMessage,
11391
+ assistantMessage: sanitizedMessage,
11132
11392
  };
11133
11393
  }
11134
11394
 
@@ -11169,6 +11429,12 @@ export class AgentSession {
11169
11429
  }
11170
11430
  }
11171
11431
  }
11432
+ messages.push({
11433
+ role: "developer",
11434
+ content: [{ type: "text", text: sideChannelNoToolsReminder }],
11435
+ attribution: "agent",
11436
+ timestamp: Date.now(),
11437
+ });
11172
11438
  messages.push({
11173
11439
  role: "user",
11174
11440
  content: [{ type: "text", text: promptText }],
@@ -11260,6 +11526,7 @@ export class AgentSession {
11260
11526
  const previousTools = [...this.agent.state.tools];
11261
11527
  const previousBaseSystemPrompt = this.#baseSystemPrompt;
11262
11528
  const previousSystemPrompt = this.agent.state.systemPrompt;
11529
+ const previousBaseSystemPromptBeforeMemoryPromotion = this.#baseSystemPromptBeforeMemoryPromotion;
11263
11530
  const previousFreshProviderSessionId = this.#freshProviderSessionId;
11264
11531
  const previousFallbackSelectedMCPToolNames = previousSessionFile
11265
11532
  ? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
@@ -11368,8 +11635,7 @@ export class AgentSession {
11368
11635
  : configuredServiceTier;
11369
11636
 
11370
11637
  if (switchingToDifferentSession) {
11371
- this.#resetHindsightConversationTrackingIfHindsight();
11372
- this.#resetMnemopiConversationTrackingIfMnemopi();
11638
+ await this.#resetMemoryContextForNewTranscript();
11373
11639
  }
11374
11640
  this.#reconnectToAgent();
11375
11641
  try {
@@ -11405,6 +11671,7 @@ export class AgentSession {
11405
11671
  this.agent.setSystemPrompt(previousSystemPrompt);
11406
11672
  }
11407
11673
  this.#baseSystemPrompt = previousBaseSystemPrompt;
11674
+ this.#baseSystemPromptBeforeMemoryPromotion = previousBaseSystemPromptBeforeMemoryPromotion;
11408
11675
  this.agent.setSystemPrompt(previousSystemPrompt);
11409
11676
  this.agent.replaceMessages(previousAgentMessages);
11410
11677
  this.agent.replaceQueues(previousSteeringMessages, previousFollowUpMessages);
@@ -11483,8 +11750,7 @@ export class AgentSession {
11483
11750
  this.#syncAgentSessionId();
11484
11751
  this.#rekeyHindsightMemoryForCurrentSessionId();
11485
11752
  this.#rekeyMnemopiMemoryForCurrentSessionId();
11486
- this.#resetHindsightConversationTrackingIfHindsight();
11487
- this.#resetMnemopiConversationTrackingIfMnemopi();
11753
+ await this.#resetMemoryContextForNewTranscript();
11488
11754
 
11489
11755
  // Reload messages from entries (works for both file and in-memory mode)
11490
11756
  const sessionContext = this.buildDisplaySessionContext();
@@ -11570,14 +11836,13 @@ export class AgentSession {
11570
11836
  content: [{ type: "text", text: question }],
11571
11837
  timestamp: Date.now(),
11572
11838
  });
11573
- this.sessionManager.appendMessage(assistantMessage);
11839
+ this.sessionManager.appendMessage(sanitizeAssistantForReparentedHistory(assistantMessage));
11574
11840
  this.#syncTodoPhasesFromBranch();
11575
11841
  this.#freshProviderSessionId = undefined;
11576
11842
  this.#syncAgentSessionId();
11577
11843
  this.#rekeyHindsightMemoryForCurrentSessionId();
11578
11844
  this.#rekeyMnemopiMemoryForCurrentSessionId();
11579
- this.#resetHindsightConversationTrackingIfHindsight();
11580
- this.#resetMnemopiConversationTrackingIfMnemopi();
11845
+ await this.#resetMemoryContextForNewTranscript();
11581
11846
 
11582
11847
  const sessionContext = this.buildDisplaySessionContext();
11583
11848
  await this.#restoreMCPSelectionsForSessionContext(sessionContext);
@@ -12341,6 +12606,41 @@ export class AgentSession {
12341
12606
  });
12342
12607
  }
12343
12608
 
12609
+ /**
12610
+ * Dump the current session's LLM-facing request context as JSON to a
12611
+ * auto-named file in `os.tmpdir()`. This is the synchronous
12612
+ * `convertToLlm`-boundary snapshot — system prompt, tools (wire schemas),
12613
+ * thinking/service tier, and converted messages — with no network round-trip
12614
+ * and no arming flag, so advisor/side requests cannot intercept it.
12615
+ *
12616
+ * The file persists on disk and may contain the same raw context/secrets
12617
+ * as `/dump`; treat the path accordingly.
12618
+ *
12619
+ * @returns the written file path, or `undefined` when there are no messages.
12620
+ */
12621
+ async dumpLlmRequestToTmpDir(): Promise<string | undefined> {
12622
+ const messages = this.messages;
12623
+ if (messages.length === 0) return undefined;
12624
+ const llmMessages = await this.convertMessagesToLlm(messages);
12625
+ const payload = {
12626
+ model: this.agent.state.model ?? null,
12627
+ thinkingLevel: this.#thinkingLevel ?? null,
12628
+ serviceTier: this.agent.serviceTier ?? null,
12629
+ systemPrompt: this.agent.state.systemPrompt,
12630
+ tools: this.agent.state.tools.map(tool => ({
12631
+ name: tool.name,
12632
+ description: tool.description,
12633
+ parameters: toolWireSchema(tool),
12634
+ ...(tool.strict !== undefined ? { strict: tool.strict } : {}),
12635
+ ...(tool.customWireName ? { customWireName: tool.customWireName } : {}),
12636
+ })),
12637
+ messages: llmMessages,
12638
+ };
12639
+ const filePath = path.join(os.tmpdir(), `omp-llm-request-${Snowflake.next()}.json`);
12640
+ await Bun.write(filePath, `${JSON.stringify(payload, null, 2)}\n`);
12641
+ return filePath;
12642
+ }
12643
+
12344
12644
  /**
12345
12645
  * Enable or disable the advisor for this session. The setting is overridden for the session,
12346
12646
  * and the runtime is started or stopped to match.