@gajae-code/coding-agent 0.12.19 → 0.12.20

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 (97) hide show
  1. package/CHANGELOG.md +20 -9
  2. package/dist/types/commands/gc.d.ts +4 -0
  3. package/dist/types/commands/sdk.d.ts +66 -0
  4. package/dist/types/config/settings-schema.d.ts +34 -0
  5. package/dist/types/gjc-runtime/gc-runtime.d.ts +97 -1
  6. package/dist/types/modes/acp/acp-agent.d.ts +33 -1
  7. package/dist/types/modes/components/bash-execution.d.ts +7 -0
  8. package/dist/types/modes/components/eval-execution.d.ts +7 -0
  9. package/dist/types/modes/utils/ui-helpers.d.ts +10 -1
  10. package/dist/types/sdk/acp/adapter.d.ts +9 -17
  11. package/dist/types/sdk/acp/index.d.ts +1 -1
  12. package/dist/types/sdk/broker/broker.d.ts +93 -1
  13. package/dist/types/sdk/broker/ensure.d.ts +17 -0
  14. package/dist/types/sdk/broker/index.d.ts +1 -0
  15. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +10 -1
  16. package/dist/types/sdk/broker/lifecycle.d.ts +81 -2
  17. package/dist/types/sdk/broker/startup-budget.d.ts +44 -0
  18. package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
  19. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +11 -1
  20. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -0
  21. package/dist/types/sdk/bus/index.d.ts +20 -9
  22. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -6
  23. package/dist/types/sdk/bus/slack-daemon.d.ts +2 -2
  24. package/dist/types/sdk/host/query/revision-store.d.ts +5 -0
  25. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  26. package/dist/types/sdk/lifecycle-session.d.ts +13 -2
  27. package/dist/types/sdk/prompt-failure.d.ts +13 -0
  28. package/dist/types/sdk/prompt-watchdog.d.ts +82 -0
  29. package/dist/types/sdk/session-reconnect.d.ts +20 -0
  30. package/dist/types/sdk/startup-capability.d.ts +1 -1
  31. package/dist/types/session/agent-session.d.ts +14 -4
  32. package/dist/types/session/blob-store.d.ts +32 -0
  33. package/dist/types/session/internal/managed-session-storage.d.ts +40 -2
  34. package/dist/types/session/internal/session-memory-sidecar.d.ts +881 -0
  35. package/dist/types/session/session-manager.d.ts +301 -27
  36. package/dist/types/session/session-storage.d.ts +170 -0
  37. package/dist/types/task/executor.d.ts +2 -1
  38. package/dist/types/tools/bash.d.ts +1 -0
  39. package/dist/types/tui/output-block.d.ts +6 -0
  40. package/package.json +8 -8
  41. package/src/cli/args.ts +75 -34
  42. package/src/commands/acp.ts +7 -1
  43. package/src/commands/gc.ts +31 -2
  44. package/src/commands/sdk.ts +141 -7
  45. package/src/config/settings-schema.ts +43 -0
  46. package/src/coordinator-mcp/server.ts +6 -1
  47. package/src/export/html/index.ts +83 -39
  48. package/src/gjc-runtime/gc-runtime.ts +1283 -4
  49. package/src/internal-urls/docs-index.generated.ts +1 -1
  50. package/src/internal-urls/local-protocol.ts +14 -0
  51. package/src/main.ts +40 -9
  52. package/src/modes/acp/acp-agent.ts +564 -37
  53. package/src/modes/acp/terminal-auth.ts +2 -2
  54. package/src/modes/components/bash-execution.ts +14 -0
  55. package/src/modes/components/eval-execution.ts +14 -0
  56. package/src/modes/controllers/command-controller.ts +63 -8
  57. package/src/modes/utils/ui-helpers.ts +81 -11
  58. package/src/sdk/acp/adapter.ts +46 -45
  59. package/src/sdk/acp/index.ts +1 -1
  60. package/src/sdk/broker/broker.ts +392 -11
  61. package/src/sdk/broker/ensure.ts +131 -61
  62. package/src/sdk/broker/index.ts +1 -0
  63. package/src/sdk/broker/lifecycle-ledger.ts +59 -5
  64. package/src/sdk/broker/lifecycle.ts +278 -63
  65. package/src/sdk/broker/startup-budget.ts +103 -0
  66. package/src/sdk/bus/chat-daemon-control.ts +9 -3
  67. package/src/sdk/bus/chat-daemon-runtime.ts +565 -29
  68. package/src/sdk/bus/conversation-store.ts +1 -1
  69. package/src/sdk/bus/discord-daemon.ts +2 -1
  70. package/src/sdk/bus/discord-live-provider.ts +34 -6
  71. package/src/sdk/bus/index.ts +118 -20
  72. package/src/sdk/bus/kind-aware-reconciliation.ts +2 -1
  73. package/src/sdk/bus/prompt-reconciliation.ts +4 -9
  74. package/src/sdk/bus/slack-daemon.ts +91 -18
  75. package/src/sdk/cli/session-cli.ts +8 -1
  76. package/src/sdk/host/query/handlers.ts +1 -1
  77. package/src/sdk/host/query/revision-store.ts +48 -12
  78. package/src/sdk/host/session-runtime.test.ts +50 -0
  79. package/src/sdk/host/session-runtime.ts +12 -2
  80. package/src/sdk/lifecycle-session.ts +26 -3
  81. package/src/sdk/mcp/server.ts +6 -1
  82. package/src/sdk/prompt-failure.ts +42 -0
  83. package/src/sdk/prompt-watchdog.ts +162 -0
  84. package/src/sdk/session-reconnect.ts +43 -0
  85. package/src/sdk/startup-capability.ts +13 -4
  86. package/src/session/agent-session.ts +525 -222
  87. package/src/session/blob-store.ts +89 -0
  88. package/src/session/internal/managed-session-scope.ts +36 -26
  89. package/src/session/internal/managed-session-storage.ts +303 -32
  90. package/src/session/internal/session-memory-sidecar.ts +2160 -0
  91. package/src/session/session-manager.ts +7027 -394
  92. package/src/session/session-storage.ts +1134 -11
  93. package/src/task/executor.ts +17 -4
  94. package/src/tools/bash.ts +109 -7
  95. package/src/tools/find.ts +55 -26
  96. package/src/tools/todo-contract.ts +40 -8
  97. package/src/tui/output-block.ts +11 -1
@@ -431,8 +431,8 @@ import type {
431
431
  RecoveryHydrationPromotionFence,
432
432
  SessionContext,
433
433
  SessionEntry,
434
- SessionManager,
435
434
  SessionManagerCloseOutcome,
435
+ SessionMemoryStats,
436
436
  } from "./session-manager";
437
437
 
438
438
  import {
@@ -441,6 +441,8 @@ import {
441
441
  getSessionMessageEntryId,
442
442
  getSessionMessageObservationId,
443
443
  SessionAppendPersistenceError,
444
+ SessionContextTooLargeError,
445
+ SessionManager,
444
446
  transferSessionMessageIdentity,
445
447
  } from "./session-manager";
446
448
  import { getEntriesForInternalRead, getSessionContextForInternalRead } from "./session-manager-internal";
@@ -744,6 +746,17 @@ type AutoCompactionTerminalStatus =
744
746
  | { kind: "skipped"; continuationScheduled?: boolean }
745
747
  | { kind: "failed" };
746
748
 
749
+ /**
750
+ * R3.2 pre-submit seam: `build` assembles the attempt messages (Phase A once,
751
+ * Phase B per attempt) and may throw the typed overflow error; `reset` drops the
752
+ * cached Phase B attempt messages so the next `build` rebuilds them (used after a
753
+ * forced compaction retry).
754
+ */
755
+ type PreSubmitBuilder = {
756
+ build: () => Promise<AgentMessage[]>;
757
+ reset: () => void;
758
+ };
759
+
747
760
  /** Options for AgentSession.prompt() */
748
761
  export interface PromptOptions {
749
762
  /** Whether to expand file-based prompt templates (default: true) */
@@ -769,8 +782,8 @@ export interface PromptOptions {
769
782
  */
770
783
  onPreflightAccepted?: () => void;
771
784
  /**
772
- * Awaitable durable-accept fence. Called after preflight and before queue mutation
773
- * or `#promptAgentWithIdleRetry`. SDK bus installs a closure that fsyncs acceptance.
785
+ * Awaitable durable-accept fence. Called after preflight and immediately before
786
+ * agent execution begins. SDK bus installs a closure that fsyncs acceptance.
774
787
  */
775
788
  onPreflightAcceptCommit?: () => void | Promise<void>;
776
789
  /** Skill-only: prepared metadata before the durable fence (path/lineCount/cleanedArgs). */
@@ -904,6 +917,7 @@ export interface SessionStats {
904
917
  premiumRequests: number;
905
918
  cost: number;
906
919
  costBreakdown?: Usage["cost"];
920
+ sessionMemory: SessionMemoryStats;
907
921
  }
908
922
 
909
923
  /** Internal marker for hook messages queued through the agent loop */
@@ -1798,7 +1812,7 @@ function deobfuscateSessionContext(context: SessionContext, obfuscator: SecretOb
1798
1812
 
1799
1813
  export class AgentSession {
1800
1814
  readonly agent: Agent;
1801
- readonly sessionManager: SessionManager;
1815
+ sessionManager: SessionManager;
1802
1816
  readonly settings: Settings;
1803
1817
  readonly memoryBackend: LazyService<MemoryBackend>;
1804
1818
  readonly notificationSessionController: NotificationSessionController | undefined;
@@ -2015,7 +2029,11 @@ export class AgentSession {
2015
2029
 
2016
2030
  // Bash execution state
2017
2031
  #bashAbortControllers = new Set<AbortController>();
2018
- #pendingBashMessages: BashExecutionMessage[] = [];
2032
+ #pendingBashMessages: Array<{
2033
+ message: BashExecutionMessage;
2034
+ onPersisted?: () => void;
2035
+ appendedToAgent: boolean;
2036
+ }> = [];
2019
2037
  #foregroundBashBackgroundRequestHandler: (() => void) | undefined;
2020
2038
 
2021
2039
  // Python execution state
@@ -2024,6 +2042,7 @@ export class AgentSession {
2024
2042
  /** Idempotent unregister handle for this session's resource-GC registration. */
2025
2043
  #unregisterResourceGc?: () => void;
2026
2044
  #unregisterRuntimeStateFinalizer?: () => void;
2045
+ #unregisterSessionMemorySettings?: () => void;
2027
2046
  /**
2028
2047
  * AsyncJobManager owned by this session (top-level only). Subagents leave
2029
2048
  * this undefined and **MUST NOT** dispose the global instance on teardown.
@@ -2031,7 +2050,11 @@ export class AgentSession {
2031
2050
  readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
2032
2051
  readonly #ownedMcpManager: MCPManager | undefined;
2033
2052
  #startupTurnBarrier: Promise<void> | undefined;
2034
- #pendingPythonMessages: PythonExecutionMessage[] = [];
2053
+ #pendingPythonMessages: Array<{
2054
+ message: PythonExecutionMessage;
2055
+ onPersisted?: () => void;
2056
+ appendedToAgent: boolean;
2057
+ }> = [];
2035
2058
  #activeEvalExecutions = new Set<Promise<unknown>>();
2036
2059
  #evalExecutionDisposing = false;
2037
2060
 
@@ -2584,14 +2607,58 @@ export class AgentSession {
2584
2607
  }
2585
2608
  }
2586
2609
 
2587
- #endInFlight(): void {
2610
+ #endInFlight(): unknown {
2588
2611
  this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
2589
- if (this.#promptInFlightCount === 0) {
2590
- this.#releasePowerAssertion();
2591
- this.#refreshTeamWorkerHeartbeat();
2612
+ if (this.#promptInFlightCount !== 0) return undefined;
2613
+
2614
+ this.#releasePowerAssertion();
2615
+ this.#refreshTeamWorkerHeartbeat();
2616
+ let flushError: unknown;
2617
+ try {
2618
+ // The turn is over, so nothing can split a tool_use/tool_result pair any
2619
+ // more: a `!`/`$` block that finished mid-stream must own its place in
2620
+ // agent state and the session now, not at the next prompt. Until it does,
2621
+ // the TUI shows output the transcript lacks and `onPersisted` stays unfired,
2622
+ // so a rebuild in that gap drops the only rendering of the execution.
2623
+ this.#flushPendingPromptMessages();
2624
+ } catch (error) {
2625
+ flushError = error;
2626
+ }
2627
+ this.#flushPendingAgentEnd();
2628
+ return flushError;
2629
+ }
2630
+ async #settleEndedInFlight(promptWait?: "publication" | "full"): Promise<void> {
2631
+ const flushError = this.#endInFlight();
2632
+ if (promptWait === "publication") {
2633
+ await this.#agentEndPublicationPromise;
2634
+ } else if (promptWait === "full") {
2635
+ await this.#agentEndHandlingPromise;
2636
+ await this.#waitForPostPromptRecovery();
2637
+ await this.#agentEndPublicationPromise;
2638
+ }
2639
+ await this.#waitForSessionSettlement();
2640
+ if (flushError) throw flushError;
2641
+ }
2642
+
2643
+ #flushPendingPromptMessages(): void {
2644
+ const errors: unknown[] = [];
2645
+ try {
2646
+ this.#flushPendingBashMessages();
2647
+ } catch (error) {
2648
+ errors.push(error);
2649
+ }
2650
+ try {
2651
+ this.#flushPendingPythonMessages();
2652
+ } catch (error) {
2653
+ errors.push(error);
2654
+ }
2655
+ try {
2592
2656
  this.#flushPendingBackgroundExchanges();
2593
- this.#flushPendingAgentEnd();
2657
+ } catch (error) {
2658
+ errors.push(error);
2594
2659
  }
2660
+ if (errors.length === 1) throw errors[0];
2661
+ if (errors.length > 1) throw new AggregateError(errors, "Multiple deferred prompt messages failed to flush");
2595
2662
  }
2596
2663
 
2597
2664
  #refreshTeamWorkerHeartbeat(): void {
@@ -2670,6 +2737,12 @@ export class AgentSession {
2670
2737
  this.agent.bindRunCancellationDomainBridge(this.#runCancellationDomains, this.#agentSessionClaimKey);
2671
2738
  this.sessionManager = config.sessionManager;
2672
2739
  this.settings = config.settings;
2740
+ this.sessionManager.setSessionMemoryMode(this.settings.get("sessionMemory.mode"));
2741
+ this.#unregisterSessionMemorySettings = this.settings.onChanged(settingPath => {
2742
+ if (settingPath === "sessionMemory.mode") {
2743
+ this.sessionManager.setSessionMemoryMode(this.settings.get("sessionMemory.mode"));
2744
+ }
2745
+ });
2673
2746
  this.memoryBackend = config.memoryBackend ?? createMemoryBackendService(this.settings);
2674
2747
  this.#workerIntegrationScheduler = new WorkerIntegrationRequestScheduler(
2675
2748
  config.workerIntegrationRequest ??
@@ -6256,6 +6329,8 @@ export class AgentSession {
6256
6329
  // No-op when this session opened no tabs. Failure is logged, not thrown.
6257
6330
  this.#unregisterResourceGc?.();
6258
6331
  this.#unregisterResourceGc = undefined;
6332
+ this.#unregisterSessionMemorySettings?.();
6333
+ this.#unregisterSessionMemorySettings = undefined;
6259
6334
  if (ownerTerminalContextFromEnvironment() === null) this.#unregisterRuntimeStateFinalizer?.();
6260
6335
  this.#unregisterRuntimeStateFinalizer = undefined;
6261
6336
  await releaseTabsForOwner(this.sessionManager.getSessionId()).catch((error: unknown) =>
@@ -6320,6 +6395,8 @@ export class AgentSession {
6320
6395
  const kernelOwnerId = this.#evalKernelOwnerId;
6321
6396
  this.#unregisterResourceGc?.();
6322
6397
  this.#unregisterResourceGc = undefined;
6398
+ this.#unregisterSessionMemorySettings?.();
6399
+ this.#unregisterSessionMemorySettings = undefined;
6323
6400
  this.#unregisterTeamWorkerAsyncJobChange?.();
6324
6401
  this.#unregisterTeamWorkerAsyncJobChange = undefined;
6325
6402
  this.#teamWorkerHeartbeat?.dispose();
@@ -7604,8 +7681,7 @@ export class AgentSession {
7604
7681
  await this.#waitForPostPromptRecovery();
7605
7682
  } finally {
7606
7683
  this.#removeEphemeralCustomMessages();
7607
- this.#endInFlight();
7608
- await this.#waitForSessionSettlement();
7684
+ await this.#settleEndedInFlight();
7609
7685
  }
7610
7686
  }
7611
7687
 
@@ -8799,10 +8875,8 @@ export class AgentSession {
8799
8875
  this.#overflowMaintenanceAttempts = 0;
8800
8876
  this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
8801
8877
  }
8802
- // Flush any pending bash messages before the new prompt
8803
- this.#flushPendingBashMessages();
8804
- this.#flushPendingPythonMessages();
8805
- this.#flushPendingBackgroundExchanges();
8878
+ // Retry anything whose previous post-turn persistence failed before the new prompt.
8879
+ this.#flushPendingPromptMessages();
8806
8880
 
8807
8881
  // Reset todo reminder count on new user prompt
8808
8882
  this.#todoReminderCount = 0;
@@ -8837,160 +8911,198 @@ export class AgentSession {
8837
8911
  ]);
8838
8912
  }
8839
8913
 
8840
- // Build messages array (session context, eager todo prelude, then active prompt message)
8841
- const messages: AgentMessage[] = [];
8842
- const planReferenceMessage = await this.#buildPlanReferenceMessage?.();
8843
- if (planReferenceMessage) {
8844
- messages.push(planReferenceMessage);
8845
- }
8846
- const planModeMessage = await this.#buildAutomaticPlanModeMessage();
8847
- if (planModeMessage) {
8848
- messages.push(planModeMessage);
8849
- }
8850
- const goalModeMessage = this.#buildAutomaticGoalModeMessage();
8851
- if (goalModeMessage) {
8852
- messages.push(goalModeMessage);
8853
- }
8854
- const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
8855
- messages.push(volatileProjectContextMessage);
8856
- const untrustedMcpServerInstructionsMessage = this.#buildUntrustedMcpServerInstructionsMessage();
8857
- if (untrustedMcpServerInstructionsMessage) messages.push(untrustedMcpServerInstructionsMessage);
8858
-
8859
- if (rosterClaim && this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch)) {
8860
- messages.push(rosterClaim.message);
8861
- } else if (rosterClaim) {
8862
- this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
8863
- }
8864
- if (options?.prependMessages) {
8865
- messages.push(...options.prependMessages);
8866
- }
8867
-
8868
- messages.push(message);
8869
-
8870
- // Early bail-out: a generation change or cancellation during setup must
8871
- // terminate preflight rather than retaining SDK prompt authority.
8872
- if (this.#isPromptPreflightCancelled(generation, preflightSignal)) {
8873
- this.#resetInjectedContextSignatures();
8874
- // A newer abort/prompt cycle superseded this preflight. Callers awaiting
8875
- // acceptance (onPreflightAccepted) must be told it never ran; direct
8876
- // callers (e.g. prompt() aborted during a TTSR wait) resolve gracefully.
8877
- if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
8878
- return;
8879
- }
8880
-
8881
- // Inject any pending "nextTurn" messages as context alongside the user message
8914
+ // R3.2: one-time Phase A products are captured inside the closure and
8915
+ // executed once after admission acceptance; Phase B reassembles the
8916
+ // attempt messages after every idle wait / compact retry. Pending
8917
+ // next-turn messages are captured WITHOUT draining — the drain moves to
8918
+ // the run-accepted wrapper (R3.3) so AgentBusy/idle/compact-before-
8919
+ // acceptance never consume them.
8882
8920
  pendingNextTurnMessageCount = this.#pendingNextTurnMessages.length;
8883
8921
  hasPendingNextTurnMessages = pendingNextTurnMessageCount > 0;
8884
- for (const msg of this.#pendingNextTurnMessages.slice(0, pendingNextTurnMessageCount)) {
8885
- messages.push(msg);
8886
- }
8887
-
8888
- // Auto-read @filepath mentions
8889
- const fileMentions = extractFileMentions(expandedText);
8890
- if (fileMentions.length > 0) {
8891
- const cwd = this.sessionManager.getCwd();
8892
- // Collect resolved paths already shown (read or mentioned) in the recent
8893
- // window so a repeat @mention emits a compact note instead of the full body.
8894
- const RECENT_MENTION_WINDOW = 40;
8895
- const recentlyShownPaths = new Set<string>();
8896
- for (const entry of this.sessionManager.getBranch().slice(-RECENT_MENTION_WINDOW)) {
8897
- if (entry.type !== "message") continue;
8898
- const msg = entry.message;
8899
- if (msg.role === "fileMention") {
8900
- for (const file of msg.files) {
8901
- if (!file.duplicate && !file.pruned) recentlyShownPaths.add(resolveReadPath(file.path, cwd));
8922
+ const promptAttribution: "user" | "agent" | undefined =
8923
+ "attribution" in message ? message.attribution : undefined;
8924
+ let phaseACompleted = false;
8925
+ let fileMentionMessages: AgentMessage[] = [];
8926
+ let beforeAgentStartResultMessages: BeforeAgentStartInternalMessage[] = [];
8927
+ const contributedMessages: BeforeAgentStartInternalMessage[] = [];
8928
+ let recallMarked = false;
8929
+ let planReferenceMessage: CustomMessage | null = null;
8930
+ // Phase B attempt cache. AgentBusy/idle and forced-compaction retries clear
8931
+ // this cache so live plan/goal/volatile/MCP overlays are rebuilt. One-shot
8932
+ // Phase A products (including the plan reference) remain stable.
8933
+ let attemptMessages: AgentMessage[] | undefined;
8934
+
8935
+ const buildPreSubmit = async (): Promise<AgentMessage[]> => {
8936
+ this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
8937
+ if (!phaseACompleted) {
8938
+ phaseACompleted = true;
8939
+ // Phase A (one-time side-effectful products; runs once).
8940
+ const fileMentions = extractFileMentions(expandedText);
8941
+ if (fileMentions.length > 0) {
8942
+ const cwd = this.sessionManager.getCwd();
8943
+ // Collect resolved paths already shown (read or mentioned) in the
8944
+ // recent window so a repeat @mention emits a compact note instead
8945
+ // of the full body.
8946
+ const RECENT_MENTION_WINDOW = 40;
8947
+ const recentlyShownPaths = new Set<string>();
8948
+ for (const entry of this.sessionManager.getBranch().slice(-RECENT_MENTION_WINDOW)) {
8949
+ if (entry.type !== "message") continue;
8950
+ const msg = entry.message;
8951
+ if (msg.role === "fileMention") {
8952
+ for (const file of msg.files) {
8953
+ if (!file.duplicate && !file.pruned) recentlyShownPaths.add(resolveReadPath(file.path, cwd));
8954
+ }
8955
+ } else if (msg.role === "toolResult") {
8956
+ const resolved = (msg.details as { resolvedPath?: unknown } | undefined)?.resolvedPath;
8957
+ if (typeof resolved === "string" && resolved)
8958
+ recentlyShownPaths.add(resolveReadPath(resolved, cwd));
8959
+ }
8960
+ }
8961
+ fileMentionMessages = await generateFileMentionMessages(fileMentions, cwd, {
8962
+ autoResizeImages: this.settings.get("images.autoResize"),
8963
+ useHashLines: resolveFileDisplayMode(this).hashLines,
8964
+ maxInlineBytes: this.settings.get("tools.fileMentionInlineBytes") * 1024,
8965
+ recentlyShownPaths,
8966
+ });
8967
+ }
8968
+ const beforeAgentStartSystemPrompt = await this.#buildSystemPromptForAgentStart(expandedText);
8969
+ hindsightRecall = this.getHindsightSessionState()?.getRecallSnippetForInjection();
8970
+ planReferenceMessage = await this.#buildPlanReferenceMessage();
8971
+ // Emit before_agent_start extension event. Race hook completion with
8972
+ // prompt cancellation so a wedged hook cannot retain SDK prompt authority.
8973
+ if (this.#extensionRunner?.hasHandlers("before_agent_start")) this.#markRetryReplayUnsafe();
8974
+ if (this.#extensionRunner) {
8975
+ const result = await this.#awaitPromptPreflight(
8976
+ generation,
8977
+ preflightSignal,
8978
+ this.#extensionRunner.emitBeforeAgentStart(
8979
+ expandedText,
8980
+ options?.images,
8981
+ beforeAgentStartSystemPrompt,
8982
+ ),
8983
+ );
8984
+ if (result?.messages) beforeAgentStartResultMessages = [...result.messages];
8985
+ if (result?.systemPrompt !== undefined) {
8986
+ this.agent.setSystemPrompt(result.systemPrompt);
8987
+ } else {
8988
+ this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
8989
+ }
8990
+ } else {
8991
+ this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
8992
+ }
8993
+ // Invoke first-party internal before-agent-start contributors. These
8994
+ // run alongside the extension runner (not via user-loaded hooks) and
8995
+ // append through the same custom-message attribution path. Errors are nonfatal.
8996
+ if (this.#beforeAgentStartContributors.length > 0) {
8997
+ for (const contributor of this.#beforeAgentStartContributors) {
8998
+ try {
8999
+ const msg = await this.#awaitPromptPreflight(
9000
+ generation,
9001
+ preflightSignal,
9002
+ contributor({
9003
+ prompt: expandedText,
9004
+ images: options?.images,
9005
+ sessionId: this.sessionId,
9006
+ }),
9007
+ );
9008
+ if (msg) contributedMessages.push(msg);
9009
+ } catch (err) {
9010
+ if (this.#isPromptPreflightCancelled(generation, preflightSignal))
9011
+ throw promptPreflightCancelledError();
9012
+ logger.debug("before_agent_start contributor failed", { error: String(err) });
9013
+ }
8902
9014
  }
8903
- } else if (msg.role === "toolResult") {
8904
- const resolved = (msg.details as { resolvedPath?: unknown } | undefined)?.resolvedPath;
8905
- if (typeof resolved === "string" && resolved) recentlyShownPaths.add(resolveReadPath(resolved, cwd));
8906
9015
  }
9016
+ this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
8907
9017
  }
8908
- const fileMentionMessages = await generateFileMentionMessages(fileMentions, cwd, {
8909
- autoResizeImages: this.settings.get("images.autoResize"),
8910
- useHashLines: resolveFileDisplayMode(this).hashLines,
8911
- maxInlineBytes: this.settings.get("tools.fileMentionInlineBytes") * 1024,
8912
- recentlyShownPaths,
8913
- });
8914
- messages.push(...fileMentionMessages);
8915
- }
8916
9018
 
8917
- const beforeAgentStartSystemPrompt = await this.#buildSystemPromptForAgentStart(expandedText);
8918
- hindsightRecall = this.getHindsightSessionState()?.getRecallSnippetForInjection();
8919
- if (hindsightRecall) {
8920
- // Recall is provider-only context for this request. It must precede the
8921
- // actual prompt but never become part of durable session history.
8922
- const promptIndex = messages.lastIndexOf(message);
8923
- messages.splice(promptIndex, 0, {
8924
- role: "custom",
8925
- customType: "hindsight-recall",
8926
- content: hindsightRecall,
8927
- display: false,
8928
- attribution: "agent",
8929
- timestamp: Date.now(),
8930
- });
8931
- }
8932
-
8933
- const promptAttribution: "user" | "agent" | undefined =
8934
- "attribution" in message ? message.attribution : undefined;
9019
+ // Phase B (attempt-dependent; re-runs after every idle wait / compact retry).
9020
+ // R3.2 overflow preflight: materialize the session context synchronously so
9021
+ // an over-budget graph throws SessionContextTooLargeError before any prompt.
9022
+ this.sessionManager.buildSessionContext();
9023
+ if (attemptMessages) {
9024
+ // Idle/AgentBusy retry: reuse the previously assembled attempt
9025
+ // messages; a superseded roster claim releases and the prompt
9026
+ // proceeds without it, matching the pre-seam reuse behavior.
9027
+ if (rosterClaim && !this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch)) {
9028
+ this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
9029
+ }
9030
+ return attemptMessages;
9031
+ }
9032
+ const messages: AgentMessage[] = [];
9033
+ const currentPlanReferenceMessage = planReferenceMessage;
9034
+ if (currentPlanReferenceMessage) {
9035
+ messages.push(currentPlanReferenceMessage);
9036
+ }
9037
+ const planModeMessage = await this.#buildAutomaticPlanModeMessage();
9038
+ if (planModeMessage) {
9039
+ messages.push(planModeMessage);
9040
+ }
9041
+ const goalModeMessage = this.#buildAutomaticGoalModeMessage();
9042
+ if (goalModeMessage) {
9043
+ messages.push(goalModeMessage);
9044
+ }
9045
+ const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
9046
+ messages.push(volatileProjectContextMessage);
9047
+ const untrustedMcpServerInstructionsMessage = this.#buildUntrustedMcpServerInstructionsMessage();
9048
+ if (untrustedMcpServerInstructionsMessage) messages.push(untrustedMcpServerInstructionsMessage);
9049
+
9050
+ // Roster: one Phase A claim, revalidated and reused on every attempt;
9051
+ // a superseded claim releases and the prompt proceeds without it.
9052
+ if (rosterClaim && this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch)) {
9053
+ messages.push(rosterClaim.message);
9054
+ } else if (rosterClaim) {
9055
+ this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
9056
+ }
9057
+ if (options?.prependMessages) {
9058
+ messages.push(...options.prependMessages);
9059
+ }
8935
9060
 
8936
- // Emit before_agent_start extension event. Race hook completion with prompt
8937
- // cancellation so a wedged hook cannot retain SDK prompt authority.
8938
- if (this.#extensionRunner?.hasHandlers("before_agent_start")) this.#markRetryReplayUnsafe();
9061
+ const promptIndex = messages.length;
9062
+ messages.push(message);
8939
9063
 
8940
- if (this.#extensionRunner) {
8941
- const result = await this.#awaitPromptPreflight(
8942
- generation,
8943
- preflightSignal,
8944
- this.#extensionRunner.emitBeforeAgentStart(expandedText, options?.images, beforeAgentStartSystemPrompt),
8945
- );
8946
- if (result?.messages) {
8947
- this.#appendBeforeAgentStartCustomMessages(messages, result.messages, promptAttribution, message.role);
9064
+ // Re-present captured pending next-turn messages (never drained here).
9065
+ for (const msg of this.#pendingNextTurnMessages.slice(0, pendingNextTurnMessageCount)) {
9066
+ messages.push(msg);
8948
9067
  }
8949
-
8950
- if (result?.systemPrompt !== undefined) {
8951
- this.agent.setSystemPrompt(result.systemPrompt);
8952
- } else {
8953
- this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
9068
+ messages.push(...fileMentionMessages);
9069
+ if (hindsightRecall) {
9070
+ // Recall is provider-only context for this request. It must precede
9071
+ // the actual prompt but never become part of durable session history.
9072
+ messages.splice(promptIndex, 0, {
9073
+ role: "custom",
9074
+ customType: "hindsight-recall",
9075
+ content: hindsightRecall,
9076
+ display: false,
9077
+ attribution: "agent",
9078
+ timestamp: Date.now(),
9079
+ });
8954
9080
  }
8955
- } else {
8956
- this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
8957
- }
8958
-
8959
- // Invoke first-party internal before-agent-start contributors. These run
8960
- // alongside the extension runner (not via user-loaded hooks) and append
8961
- // through the same custom-message attribution path. Errors are nonfatal.
8962
- if (this.#beforeAgentStartContributors.length > 0) {
8963
- const contributed: BeforeAgentStartInternalMessage[] = [];
8964
- for (const contributor of this.#beforeAgentStartContributors) {
8965
- try {
8966
- const msg = await this.#awaitPromptPreflight(
8967
- generation,
8968
- preflightSignal,
8969
- contributor({
8970
- prompt: expandedText,
8971
- images: options?.images,
8972
- sessionId: this.sessionId,
8973
- }),
8974
- );
8975
- if (msg) contributed.push(msg);
8976
- } catch (err) {
8977
- if (this.#isPromptPreflightCancelled(generation, preflightSignal))
8978
- throw promptPreflightCancelledError();
8979
- logger.debug("before_agent_start contributor failed", { error: String(err) });
8980
- }
9081
+ if (beforeAgentStartResultMessages.length > 0) {
9082
+ this.#appendBeforeAgentStartCustomMessages(
9083
+ messages,
9084
+ beforeAgentStartResultMessages,
9085
+ promptAttribution,
9086
+ message.role,
9087
+ );
8981
9088
  }
8982
- this.#appendBeforeAgentStartCustomMessages(messages, contributed, promptAttribution, message.role);
8983
- }
8984
-
8985
- // Abort can race asynchronous preflight work. The injection signatures were
8986
- // consumed while building context, but no prompt was accepted, so reset them.
8987
- if (this.#isPromptPreflightCancelled(generation, preflightSignal)) {
8988
- this.#resetInjectedContextSignatures();
8989
- // Ack-waiting callers are told the preflight never ran; direct callers
8990
- // (aborted after setup) resolve gracefully as before f24f46ff5.
8991
- if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
8992
- return;
8993
- }
9089
+ if (contributedMessages.length > 0) {
9090
+ this.#appendBeforeAgentStartCustomMessages(
9091
+ messages,
9092
+ contributedMessages,
9093
+ promptAttribution,
9094
+ message.role,
9095
+ );
9096
+ }
9097
+ attemptMessages = messages;
9098
+ return messages;
9099
+ };
9100
+ const preSubmit: PreSubmitBuilder = {
9101
+ build: buildPreSubmit,
9102
+ reset: () => {
9103
+ attemptMessages = undefined;
9104
+ },
9105
+ };
8994
9106
 
8995
9107
  const agentPromptOptions = {
8996
9108
  ...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
@@ -8999,21 +9111,31 @@ export class AgentSession {
8999
9111
  this.#acceptRunHandle(handle);
9000
9112
  options?.onRunAccepted?.(handle);
9001
9113
  options?.admissionLease?.release();
9002
- if (hindsightRecall) this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
9114
+ // R3.3: the accepted-run wrapper is the exact acceptance boundary —
9115
+ // pending next-turn drain and the exactly-once recall mark live here.
9116
+ if (hindsightRecall && !recallMarked) {
9117
+ recallMarked = true;
9118
+ this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
9119
+ }
9120
+ if (pendingNextTurnMessageCount > 0) {
9121
+ this.#pendingNextTurnMessages.splice(0, pendingNextTurnMessageCount);
9122
+ }
9123
+ if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
9003
9124
  },
9004
9125
  };
9005
- if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
9006
- else options?.onPreflightAccepted?.();
9007
9126
  if (options?.onFinalPreflight && !(await options.onFinalPreflight({ hasPendingNextTurnMessages }))) {
9008
9127
  this.#resetInjectedContextSignatures();
9009
9128
  return;
9010
9129
  }
9011
- this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
9012
- if (pendingNextTurnMessageCount > 0) {
9013
- this.#pendingNextTurnMessages.splice(0, pendingNextTurnMessageCount);
9014
- }
9015
- if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
9016
- await this.#promptAgentWithIdleRetry(messages, agentPromptOptions, predecessorAgentEndHold);
9130
+ await this.#promptAgentWithIdleRetry(preSubmit, agentPromptOptions, predecessorAgentEndHold, {
9131
+ signal: preflightSignal,
9132
+ resourceRunId: this.#runResourceLeaseContext.getStore()?.resourceRunId,
9133
+ onPreflightAccepted: () => {
9134
+ this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
9135
+ if (options?.onPreflightAcceptCommit) return options.onPreflightAcceptCommit();
9136
+ options?.onPreflightAccepted?.();
9137
+ },
9138
+ });
9017
9139
  const terminalAssistant = this.#findLastAssistantMessage();
9018
9140
  if (
9019
9141
  rosterClaim &&
@@ -9048,14 +9170,7 @@ export class AgentSession {
9048
9170
  this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
9049
9171
  }
9050
9172
  this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
9051
- this.#endInFlight();
9052
- if (options?.skipPostPromptRecoveryWait) {
9053
- await this.#agentEndPublicationPromise;
9054
- } else {
9055
- await this.#agentEndHandlingPromise;
9056
- await this.#waitForPostPromptRecovery();
9057
- await this.#agentEndPublicationPromise;
9058
- }
9173
+ await this.#settleEndedInFlight(options?.skipPostPromptRecoveryWait ? "publication" : "full");
9059
9174
  }
9060
9175
  }
9061
9176
 
@@ -10712,19 +10827,56 @@ export class AgentSession {
10712
10827
  // Flush current session to ensure all entries are written
10713
10828
  await this.sessionManager.flush();
10714
10829
 
10715
- // Prepare the copied successor and complete local:// readiness while all
10716
- // public manager getters remain bound to the predecessor.
10717
- const prepared = await this.sessionManager.prepareFork();
10718
- if (!prepared) {
10719
- return false;
10720
- }
10721
- try {
10722
- await initializeLocalRoot(this.#localProtocolOptions(prepared));
10723
- await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
10724
- this.sessionManager.commitPreparedNewSession(prepared);
10830
+ const boundedColdForkEligible =
10831
+ this.sessionManager.getSessionMemoryStats().coldRetirementActive &&
10832
+ !this.sessionManager.isManagedDestination() &&
10833
+ previousSessionFile !== undefined;
10834
+ if (boundedColdForkEligible) {
10835
+ const previousManager = this.sessionManager;
10836
+ const forkedManager = await SessionManager.forkFrom(
10837
+ previousSessionFile,
10838
+ previousManager.getCwd(),
10839
+ SessionManager.explicitDestination(previousManager.getSessionDir()),
10840
+ undefined,
10841
+ "copy-retain",
10842
+ this.settings.get("sessionMemory.mode"),
10843
+ );
10844
+ try {
10845
+ await initializeLocalRoot({
10846
+ getArtifactsDir: () => forkedManager.getArtifactsDir(),
10847
+ isManagedDestination: () => false,
10848
+ getManagedLegacyLocalMigrationSource: () => null,
10849
+ getSessionId: () => forkedManager.getSessionId(),
10850
+ });
10851
+ await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
10852
+ } catch (error) {
10853
+ const forkedFile = forkedManager.getSessionFile();
10854
+ await forkedManager.close();
10855
+ if (forkedFile) await previousManager.discardUncommittedSession(forkedFile);
10856
+ throw error;
10857
+ }
10858
+ this.sessionManager = forkedManager;
10859
+ try {
10860
+ await previousManager.close();
10861
+ } catch (error) {
10862
+ logger.warn("Previous session close failed after bounded fork adoption", {
10863
+ error: error instanceof Error ? error.message : String(error),
10864
+ });
10865
+ }
10725
10866
  await this.#runToolSessionTransitionCleanups();
10726
- } catch (error) {
10727
- throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
10867
+ } else {
10868
+ // Prepare the copied successor and complete local:// readiness while all
10869
+ // public manager getters remain bound to the predecessor.
10870
+ const prepared = await this.sessionManager.prepareFork();
10871
+ if (!prepared) return false;
10872
+ try {
10873
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
10874
+ await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
10875
+ this.sessionManager.commitPreparedNewSession(prepared);
10876
+ await this.#runToolSessionTransitionCleanups();
10877
+ } catch (error) {
10878
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
10879
+ }
10728
10880
  }
10729
10881
  this.#syncAgentSessionId();
10730
10882
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
@@ -13363,13 +13515,15 @@ export class AgentSession {
13363
13515
  }
13364
13516
  }
13365
13517
  }
13518
+ const sessionMemory = this.sessionManager.getSessionMemoryStats();
13366
13519
  return {
13367
13520
  heapUsedBytes: process.memoryUsage().heapUsed,
13368
13521
  providerBytes,
13369
13522
  messageCount: this.state.messages.length,
13370
13523
  imageBytes,
13371
13524
  sessionResidentImageBytes: this.sessionManager.getResidentImageBytes(),
13372
- materializedResidentBytes: this.#streamingEditFileCache.totalBytes,
13525
+ materializedResidentBytes:
13526
+ this.#streamingEditFileCache.totalBytes + sessionMemory.hotRegionBytes + sessionMemory.metaDescriptorBytes,
13373
13527
  tuiChatChildren: retainedMemory.tuiChatChildren ?? 0,
13374
13528
  tuiCachedRenderBytes: retainedMemory.tuiCachedRenderBytes ?? 0,
13375
13529
  };
@@ -13777,11 +13931,6 @@ export class AgentSession {
13777
13931
  `(Reminder ${this.#todoReminderCount}/${remindersMax})\n` +
13778
13932
  `</system-reminder>`;
13779
13933
 
13780
- logger.debug("Todo completion: sending reminder", {
13781
- incomplete: incomplete.length,
13782
- attempt: this.#todoReminderCount,
13783
- });
13784
-
13785
13934
  // Emit event for UI to render notification
13786
13935
  await this.#emitSessionEvent({
13787
13936
  type: "todo_reminder",
@@ -13790,7 +13939,27 @@ export class AgentSession {
13790
13939
  maxAttempts: remindersMax,
13791
13940
  });
13792
13941
 
13793
- // Inject reminder and continue the conversation
13942
+ // Consumers that cannot represent a server-initiated turn (ACP v1 clients, SDK
13943
+ // hosts) keep reporting the prompt as running until the terminal `agent_end` is
13944
+ // published. Injecting here would park that terminal behind a continuation hold
13945
+ // (`#scheduleAgentContinue` -> `#reserveDeferredAgentEndForContinuation`), so a
13946
+ // turn that already delivered its final answer would never settle — and there is
13947
+ // no interactive consumer to act on the nudge. Those hosts get the advisory
13948
+ // event above and nothing else.
13949
+ if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
13950
+ logger.debug("Todo completion: advisory reminder only", {
13951
+ incomplete: incomplete.length,
13952
+ attempt: this.#todoReminderCount,
13953
+ });
13954
+ return;
13955
+ }
13956
+
13957
+ logger.debug("Todo completion: sending reminder", {
13958
+ incomplete: incomplete.length,
13959
+ attempt: this.#todoReminderCount,
13960
+ });
13961
+
13962
+ // Inject reminder and continue conversation
13794
13963
  this.agent.appendMessage({
13795
13964
  role: "developer",
13796
13965
  content: [{ type: "text", text: reminder }],
@@ -15077,6 +15246,9 @@ export class AgentSession {
15077
15246
  )
15078
15247
  );
15079
15248
  }
15249
+ #isIdleStreamStallErrorMessage(errorMessage: string): boolean {
15250
+ return /stream stalled while waiting for the next event/i.test(errorMessage);
15251
+ }
15080
15252
 
15081
15253
  #isFirstEventTimeoutErrorMessage(errorMessage: string): boolean {
15082
15254
  // First-event timeout: the stream watchdog aborted because no event
@@ -15133,8 +15305,8 @@ export class AgentSession {
15133
15305
  /**
15134
15306
  * Ordered retry classification: typed safety stop (surface) -> legacy safety stop
15135
15307
  * (surface) -> overflow (compaction) -> terminal (surface) -> usage_limit
15136
- * (rotation) -> first_event_timeout (bounded retry) -> transient (unbounded retry) ->
15137
- * unknown (bounded retry).
15308
+ * (rotation) -> first_event_timeout (bounded retry) -> transient (unbounded retry,
15309
+ * except canonical idle-stream stalls bounded downstream) -> unknown (bounded retry).
15138
15310
  */
15139
15311
  #classifyErrorForRetry(message: AssistantMessage): RetryErrorClassification {
15140
15312
  if (message.stopReason !== "error") return "none";
@@ -15692,7 +15864,10 @@ export class AgentSession {
15692
15864
  return false;
15693
15865
  }
15694
15866
  }
15695
- const legacyUnbounded = !managedFallback && classification === "transient";
15867
+ const legacyUnbounded =
15868
+ !managedFallback &&
15869
+ classification === "transient" &&
15870
+ !this.#isIdleStreamStallErrorMessage(message.errorMessage ?? "");
15696
15871
  const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
15697
15872
  const failedSelector = managedFallback ? controller.currentSelector() : undefined;
15698
15873
  let outcome = managedFallback
@@ -15958,16 +16133,25 @@ export class AgentSession {
15958
16133
  }
15959
16134
 
15960
16135
  async #promptAgentWithIdleRetry(
15961
- messages: AgentMessage[],
16136
+ preSubmit: PreSubmitBuilder,
15962
16137
  options?: {
15963
16138
  toolChoice?: ToolChoice;
15964
16139
  fallbackManaged?: boolean;
15965
16140
  onRunAccepted?: (handle: AttemptRunHandle) => void;
15966
16141
  },
15967
16142
  predecessorAgentEndHold?: symbol,
16143
+ seam?: {
16144
+ signal?: AbortSignal;
16145
+ resourceRunId?: string;
16146
+ onPreflightAccepted?: () => void | Promise<void>;
16147
+ },
15968
16148
  ): Promise<void> {
15969
16149
  const deadline = Date.now() + 30_000;
15970
16150
  let continuationHold = predecessorAgentEndHold;
16151
+ // R3.2 helper-local compact-once flag: fresh per prompt; the inline retry
16152
+ // re-runs Phase B via preSubmit after the forced compaction.
16153
+ let overflowRetried = false;
16154
+ let preflightAccepted = false;
15971
16155
  for (;;) {
15972
16156
  try {
15973
16157
  const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(
@@ -15975,11 +16159,46 @@ export class AgentSession {
15975
16159
  );
15976
16160
  continuationHold = undefined;
15977
16161
  try {
16162
+ const messages = await preSubmit.build();
16163
+ if (!preflightAccepted) {
16164
+ await seam?.onPreflightAccepted?.();
16165
+ preflightAccepted = true;
16166
+ }
15978
16167
  await this.agent.prompt(messages, options);
15979
16168
  this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
15980
16169
  return;
15981
16170
  } catch (error) {
15982
16171
  this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
16172
+ if (
16173
+ error instanceof SessionContextTooLargeError &&
16174
+ !overflowRetried &&
16175
+ this.settings.get("sessionMemory.contextOverflowRecovery")
16176
+ ) {
16177
+ // D7: exactly one forced, no-continuation compaction
16178
+ // (`willRetry:false`, `continueAfterMaintenance:false`), then
16179
+ // exactly one inline retry. Any other terminal status rethrows
16180
+ // the ORIGINAL typed error with measurements.
16181
+ overflowRetried = true;
16182
+ let compacted = false;
16183
+ try {
16184
+ const status = await this.#runAutoCompaction("overflow", false, false, {
16185
+ force: true,
16186
+ continueAfterMaintenance: false,
16187
+ signal: seam?.signal,
16188
+ resourceRunId: seam?.resourceRunId,
16189
+ });
16190
+ compacted = status.kind === "compacted";
16191
+ } catch {
16192
+ compacted = false;
16193
+ }
16194
+ if (compacted) {
16195
+ // Post-compaction Phase B must rebuild overlays (the
16196
+ // injected-context signatures were reset by
16197
+ // #applyCompactionPostAppend), so drop the cached attempt.
16198
+ preSubmit.reset();
16199
+ continue;
16200
+ }
16201
+ }
15983
16202
  throw error;
15984
16203
  }
15985
16204
  } catch (err) {
@@ -15990,6 +16209,7 @@ export class AgentSession {
15990
16209
  throw new Error("Timed out waiting for prior agent run to finish before prompting.");
15991
16210
  }
15992
16211
  await this.agent.waitForIdle();
16212
+ preSubmit.reset();
15993
16213
  }
15994
16214
  }
15995
16215
  }
@@ -16086,11 +16306,13 @@ export class AgentSession {
16086
16306
  * @param command The bash command to execute
16087
16307
  * @param onChunk Optional streaming callback for output
16088
16308
  * @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)
16309
+ * @param options.onPersisted Called once the execution's message is in session state
16310
+ * (immediately when idle, at the post-turn flush while streaming)
16089
16311
  */
16090
16312
  async executeBash(
16091
16313
  command: string,
16092
16314
  onChunk?: (chunk: string) => void,
16093
- options?: { excludeFromContext?: boolean },
16315
+ options?: { excludeFromContext?: boolean; onPersisted?: () => void },
16094
16316
  ): Promise<BashResult> {
16095
16317
  const excludeFromContext = options?.excludeFromContext === true;
16096
16318
  this.#markRetryReplayUnsafe();
@@ -16145,7 +16367,11 @@ export class AgentSession {
16145
16367
  * Record a bash execution result in session history.
16146
16368
  * Used by executeBash and by extensions that handle bash execution themselves.
16147
16369
  */
16148
- recordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {
16370
+ recordBashResult(
16371
+ command: string,
16372
+ result: BashResult,
16373
+ options?: { excludeFromContext?: boolean; onPersisted?: () => void },
16374
+ ): void {
16149
16375
  const meta = outputMeta().truncationFromSummary(result, { direction: "tail" }).get();
16150
16376
  const bashMessage: BashExecutionMessage = {
16151
16377
  role: "bashExecution",
@@ -16162,13 +16388,18 @@ export class AgentSession {
16162
16388
  // If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
16163
16389
  if (this.isStreaming) {
16164
16390
  // Queue for later - will be flushed on agent_end
16165
- this.#pendingBashMessages.push(bashMessage);
16391
+ this.#pendingBashMessages.push({
16392
+ message: bashMessage,
16393
+ onPersisted: options?.onPersisted,
16394
+ appendedToAgent: false,
16395
+ });
16166
16396
  } else {
16167
16397
  // Add to agent state immediately
16168
16398
  this.agent.appendMessage(bashMessage);
16169
16399
 
16170
16400
  // Save to session
16171
16401
  this.sessionManager.appendMessage(bashMessage);
16402
+ options?.onPersisted?.();
16172
16403
  }
16173
16404
  }
16174
16405
 
@@ -16196,17 +16427,7 @@ export class AgentSession {
16196
16427
  * Called after agent turn completes to maintain proper message ordering.
16197
16428
  */
16198
16429
  #flushPendingBashMessages(): void {
16199
- if (this.#pendingBashMessages.length === 0) return;
16200
-
16201
- for (const bashMessage of this.#pendingBashMessages) {
16202
- // Add to agent state
16203
- this.agent.appendMessage(bashMessage);
16204
-
16205
- // Save to session
16206
- this.sessionManager.appendMessage(bashMessage);
16207
- }
16208
-
16209
- this.#pendingBashMessages = [];
16430
+ this.#flushPendingExecutionMessages(this.#pendingBashMessages, "bash");
16210
16431
  }
16211
16432
 
16212
16433
  // =========================================================================
@@ -16219,11 +16440,13 @@ export class AgentSession {
16219
16440
  * @param code The Python code to execute
16220
16441
  * @param onChunk Optional streaming callback for output
16221
16442
  * @param options.excludeFromContext If true, execution won't be sent to LLM ($$ prefix)
16443
+ * @param options.onPersisted Called once the execution's message is in session state
16444
+ * (immediately when idle, at the post-turn flush while streaming)
16222
16445
  */
16223
16446
  async executePython(
16224
16447
  code: string,
16225
16448
  onChunk?: (chunk: string) => void,
16226
- options?: { excludeFromContext?: boolean },
16449
+ options?: { excludeFromContext?: boolean; onPersisted?: () => void },
16227
16450
  ): Promise<PythonResult> {
16228
16451
  const excludeFromContext = options?.excludeFromContext === true;
16229
16452
  this.#markRetryReplayUnsafe();
@@ -16291,7 +16514,11 @@ export class AgentSession {
16291
16514
  /**
16292
16515
  * Record a Python execution result in session history.
16293
16516
  */
16294
- recordPythonResult(code: string, result: PythonResult, options?: { excludeFromContext?: boolean }): void {
16517
+ recordPythonResult(
16518
+ code: string,
16519
+ result: PythonResult,
16520
+ options?: { excludeFromContext?: boolean; onPersisted?: () => void },
16521
+ ): void {
16295
16522
  const meta = outputMeta().truncationFromSummary(result, { direction: "tail" }).get();
16296
16523
  const pythonMessage: PythonExecutionMessage = {
16297
16524
  role: "pythonExecution",
@@ -16307,10 +16534,15 @@ export class AgentSession {
16307
16534
 
16308
16535
  // If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
16309
16536
  if (this.isStreaming) {
16310
- this.#pendingPythonMessages.push(pythonMessage);
16537
+ this.#pendingPythonMessages.push({
16538
+ message: pythonMessage,
16539
+ onPersisted: options?.onPersisted,
16540
+ appendedToAgent: false,
16541
+ });
16311
16542
  } else {
16312
16543
  this.agent.appendMessage(pythonMessage);
16313
16544
  this.sessionManager.appendMessage(pythonMessage);
16545
+ options?.onPersisted?.();
16314
16546
  }
16315
16547
  }
16316
16548
 
@@ -16369,14 +16601,84 @@ export class AgentSession {
16369
16601
  * Flush pending Python messages to agent state and session.
16370
16602
  */
16371
16603
  #flushPendingPythonMessages(): void {
16372
- if (this.#pendingPythonMessages.length === 0) return;
16604
+ this.#flushPendingExecutionMessages(this.#pendingPythonMessages, "python");
16605
+ }
16373
16606
 
16374
- for (const pythonMessage of this.#pendingPythonMessages) {
16375
- this.agent.appendMessage(pythonMessage);
16376
- this.sessionManager.appendMessage(pythonMessage);
16607
+ #flushPendingExecutionMessages<T extends BashExecutionMessage | PythonExecutionMessage>(
16608
+ pendingMessages: Array<{ message: T; onPersisted?: () => void; appendedToAgent: boolean }>,
16609
+ kind: "bash" | "python",
16610
+ ): void {
16611
+ if (pendingMessages.length === 0) return;
16612
+
16613
+ const total = pendingMessages.length;
16614
+ const remaining: typeof pendingMessages = [];
16615
+ const errors: unknown[] = [];
16616
+ const persisted: string[] = [];
16617
+ let callbackFailureCount = 0;
16618
+ let persistenceBlocked = false;
16619
+ let agentAppendBlocked = false;
16620
+ for (const pending of pendingMessages) {
16621
+ if (!pending.appendedToAgent) {
16622
+ if (agentAppendBlocked) {
16623
+ remaining.push(pending);
16624
+ continue;
16625
+ }
16626
+ try {
16627
+ this.agent.appendMessage(pending.message);
16628
+ pending.appendedToAgent = true;
16629
+ } catch (error) {
16630
+ agentAppendBlocked = true;
16631
+ persistenceBlocked = true;
16632
+ remaining.push(pending);
16633
+ errors.push(error);
16634
+ continue;
16635
+ }
16636
+ }
16637
+
16638
+ if (persistenceBlocked) {
16639
+ remaining.push(pending);
16640
+ continue;
16641
+ }
16642
+
16643
+ try {
16644
+ this.sessionManager.appendMessage(pending.message);
16645
+ } catch (error) {
16646
+ // Session entries form a leaf-linked transcript. Once one append fails,
16647
+ // later entries must remain queued behind it or a retry would append the
16648
+ // failed entry after messages that originally followed it.
16649
+ persistenceBlocked = true;
16650
+ remaining.push(pending);
16651
+ errors.push(error);
16652
+ continue;
16653
+ }
16654
+
16655
+ persisted.push("command" in pending.message ? pending.message.command : pending.message.code);
16656
+ try {
16657
+ pending.onPersisted?.();
16658
+ } catch (error) {
16659
+ callbackFailureCount++;
16660
+ errors.push(error);
16661
+ }
16377
16662
  }
16378
16663
 
16379
- this.#pendingPythonMessages = [];
16664
+ pendingMessages.splice(0, pendingMessages.length, ...remaining);
16665
+ if (errors.length === 0) return;
16666
+
16667
+ const persistedCount = total - remaining.length;
16668
+ const persistenceFailureCount = remaining.length;
16669
+ const persistenceSummary =
16670
+ persistenceFailureCount > 0
16671
+ ? `Failed to persist ${persistenceFailureCount} of ${total} deferred ${kind} execution message${total === 1 ? "" : "s"}; ${persistedCount} persisted, failed messages remain pending for retry`
16672
+ : `Persisted all ${total} deferred ${kind} execution message${total === 1 ? "" : "s"}`;
16673
+ const callbackSummary =
16674
+ callbackFailureCount > 0
16675
+ ? `; ${callbackFailureCount} onPersisted callback${callbackFailureCount === 1 ? "" : "s"} failed`
16676
+ : "";
16677
+ const pending = remaining.map(item => ("command" in item.message ? item.message.command : item.message.code));
16678
+ throw new AggregateError(
16679
+ errors,
16680
+ `${persistenceSummary}${callbackSummary}; persisted: ${JSON.stringify(persisted)}; pending: ${JSON.stringify(pending)}`,
16681
+ );
16380
16682
  }
16381
16683
 
16382
16684
  // =========================================================================
@@ -17816,6 +18118,7 @@ export class AgentSession {
17816
18118
  cost: totalCost,
17817
18119
  ...(hasCompleteCostBreakdown ? { costBreakdown: totalCostBreakdown } : {}),
17818
18120
  premiumRequests: totalPremiumRequests,
18121
+ sessionMemory: this.sessionManager.getSessionMemoryStats(),
17819
18122
  };
17820
18123
  }
17821
18124