@code-yeongyu/senpi 2026.8.5 → 2026.8.7

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 (127) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/dist/core/agent-abort-provenance.d.ts +20 -0
  3. package/dist/core/agent-abort-provenance.d.ts.map +1 -0
  4. package/dist/core/agent-abort-provenance.js +83 -0
  5. package/dist/core/agent-abort-provenance.js.map +1 -0
  6. package/dist/core/agent-session.d.ts +15 -1
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +123 -80
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/agent-settled-delivery.d.ts +9 -0
  11. package/dist/core/agent-settled-delivery.d.ts.map +1 -0
  12. package/dist/core/agent-settled-delivery.js +25 -0
  13. package/dist/core/agent-settled-delivery.js.map +1 -0
  14. package/dist/core/extensions/builtin/bash-timeout/index.d.ts +2 -2
  15. package/dist/core/extensions/builtin/bash-timeout/index.d.ts.map +1 -1
  16. package/dist/core/extensions/builtin/bash-timeout/index.js +17 -20
  17. package/dist/core/extensions/builtin/bash-timeout/index.js.map +1 -1
  18. package/dist/core/extensions/builtin/bash-timeout/timeout.d.ts +7 -12
  19. package/dist/core/extensions/builtin/bash-timeout/timeout.d.ts.map +1 -1
  20. package/dist/core/extensions/builtin/bash-timeout/timeout.js +11 -19
  21. package/dist/core/extensions/builtin/bash-timeout/timeout.js.map +1 -1
  22. package/dist/core/extensions/builtin/compaction/index.js +3 -3
  23. package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
  24. package/dist/core/extensions/builtin/compaction/per-turn-cap.d.ts +8 -8
  25. package/dist/core/extensions/builtin/compaction/per-turn-cap.d.ts.map +1 -1
  26. package/dist/core/extensions/builtin/compaction/per-turn-cap.js +9 -16
  27. package/dist/core/extensions/builtin/compaction/per-turn-cap.js.map +1 -1
  28. package/dist/core/extensions/builtin/goal/agent-end-continuation.d.ts +11 -0
  29. package/dist/core/extensions/builtin/goal/agent-end-continuation.d.ts.map +1 -0
  30. package/dist/core/extensions/builtin/goal/agent-end-continuation.js +13 -0
  31. package/dist/core/extensions/builtin/goal/agent-end-continuation.js.map +1 -0
  32. package/dist/core/extensions/builtin/goal/cache-warm.d.ts +0 -2
  33. package/dist/core/extensions/builtin/goal/cache-warm.d.ts.map +1 -1
  34. package/dist/core/extensions/builtin/goal/cache-warm.js +0 -34
  35. package/dist/core/extensions/builtin/goal/cache-warm.js.map +1 -1
  36. package/dist/core/extensions/builtin/goal/continuation.d.ts +1 -1
  37. package/dist/core/extensions/builtin/goal/continuation.d.ts.map +1 -1
  38. package/dist/core/extensions/builtin/goal/continuation.js +2 -0
  39. package/dist/core/extensions/builtin/goal/continuation.js.map +1 -1
  40. package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
  41. package/dist/core/extensions/builtin/goal/index.js +17 -19
  42. package/dist/core/extensions/builtin/goal/index.js.map +1 -1
  43. package/dist/core/extensions/builtin/goal/last-assistant-message.d.ts +5 -0
  44. package/dist/core/extensions/builtin/goal/last-assistant-message.d.ts.map +1 -0
  45. package/dist/core/extensions/builtin/goal/last-assistant-message.js +9 -0
  46. package/dist/core/extensions/builtin/goal/last-assistant-message.js.map +1 -0
  47. package/dist/core/extensions/builtin/goal/monitor-continuation-types.d.ts +23 -0
  48. package/dist/core/extensions/builtin/goal/monitor-continuation-types.d.ts.map +1 -0
  49. package/dist/core/extensions/builtin/goal/monitor-continuation-types.js +2 -0
  50. package/dist/core/extensions/builtin/goal/monitor-continuation-types.js.map +1 -0
  51. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts +4 -10
  52. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts.map +1 -1
  53. package/dist/core/extensions/builtin/goal/monitor-continuation.js +31 -21
  54. package/dist/core/extensions/builtin/goal/monitor-continuation.js.map +1 -1
  55. package/dist/core/extensions/builtin/goal/terminal-provider-error.d.ts +3 -0
  56. package/dist/core/extensions/builtin/goal/terminal-provider-error.d.ts.map +1 -0
  57. package/dist/core/extensions/builtin/goal/terminal-provider-error.js +10 -0
  58. package/dist/core/extensions/builtin/goal/terminal-provider-error.js.map +1 -0
  59. package/dist/core/extensions/builtin/gpt-apply-patch/apply.d.ts.map +1 -1
  60. package/dist/core/extensions/builtin/gpt-apply-patch/apply.js +55 -11
  61. package/dist/core/extensions/builtin/gpt-apply-patch/apply.js.map +1 -1
  62. package/dist/core/extensions/builtin/gpt-apply-patch/preview-format.d.ts.map +1 -1
  63. package/dist/core/extensions/builtin/gpt-apply-patch/preview-format.js +8 -5
  64. package/dist/core/extensions/builtin/gpt-apply-patch/preview-format.js.map +1 -1
  65. package/dist/core/extensions/builtin/gpt-apply-patch/preview.d.ts +2 -0
  66. package/dist/core/extensions/builtin/gpt-apply-patch/preview.d.ts.map +1 -1
  67. package/dist/core/extensions/builtin/gpt-apply-patch/preview.js +39 -7
  68. package/dist/core/extensions/builtin/gpt-apply-patch/preview.js.map +1 -1
  69. package/dist/core/extensions/builtin/gpt-apply-patch/types.d.ts +1 -0
  70. package/dist/core/extensions/builtin/gpt-apply-patch/types.d.ts.map +1 -1
  71. package/dist/core/extensions/builtin/gpt-apply-patch/types.js.map +1 -1
  72. package/dist/core/extensions/builtin/terminal/tools/bash.d.ts.map +1 -1
  73. package/dist/core/extensions/builtin/terminal/tools/bash.js +22 -10
  74. package/dist/core/extensions/builtin/terminal/tools/bash.js.map +1 -1
  75. package/dist/core/extensions/builtin/terminal/tools/foreground-window.d.ts +11 -0
  76. package/dist/core/extensions/builtin/terminal/tools/foreground-window.d.ts.map +1 -0
  77. package/dist/core/extensions/builtin/terminal/tools/foreground-window.js +16 -0
  78. package/dist/core/extensions/builtin/terminal/tools/foreground-window.js.map +1 -0
  79. package/dist/core/extensions/builtin/terminal/tools/sleep-wait.d.ts +28 -0
  80. package/dist/core/extensions/builtin/terminal/tools/sleep-wait.d.ts.map +1 -0
  81. package/dist/core/extensions/builtin/terminal/tools/sleep-wait.js +75 -0
  82. package/dist/core/extensions/builtin/terminal/tools/sleep-wait.js.map +1 -0
  83. package/dist/core/extensions/builtin/tps.d.ts.map +1 -1
  84. package/dist/core/extensions/builtin/tps.js +3 -3
  85. package/dist/core/extensions/builtin/tps.js.map +1 -1
  86. package/dist/core/extensions/builtin/ttsr/index.d.ts.map +1 -1
  87. package/dist/core/extensions/builtin/ttsr/index.js +35 -34
  88. package/dist/core/extensions/builtin/ttsr/index.js.map +1 -1
  89. package/dist/core/extensions/runner.d.ts.map +1 -1
  90. package/dist/core/extensions/runner.js +2 -2
  91. package/dist/core/extensions/runner.js.map +1 -1
  92. package/dist/core/extensions/types.d.ts +2 -2
  93. package/dist/core/extensions/types.d.ts.map +1 -1
  94. package/dist/core/extensions/types.js.map +1 -1
  95. package/dist/core/retry-fallback/hint-policy.d.ts +14 -0
  96. package/dist/core/retry-fallback/hint-policy.d.ts.map +1 -1
  97. package/dist/core/retry-fallback/hint-policy.js +14 -0
  98. package/dist/core/retry-fallback/hint-policy.js.map +1 -1
  99. package/dist/modes/interactive/components/assistant-message.d.ts +0 -1
  100. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  101. package/dist/modes/interactive/components/assistant-message.js +8 -128
  102. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  103. package/dist/modes/interactive/components/assistant-render-descriptors.d.ts +17 -0
  104. package/dist/modes/interactive/components/assistant-render-descriptors.d.ts.map +1 -0
  105. package/dist/modes/interactive/components/assistant-render-descriptors.js +133 -0
  106. package/dist/modes/interactive/components/assistant-render-descriptors.js.map +1 -0
  107. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +41 -0
  108. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  109. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/render.ts +2 -1
  110. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  111. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts +30 -0
  112. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts.map +1 -1
  113. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +30 -0
  114. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js.map +1 -1
  115. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  116. package/node_modules/@earendil-works/pi-ai/dist/providers/data/anthropic.json +1 -1
  117. package/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
  118. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  119. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  120. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  121. package/node_modules/@earendil-works/pi-ai/package.json +1 -1
  122. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  123. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
  124. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +14 -1
  125. package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
  126. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  127. package/package.json +7 -7
@@ -24,6 +24,8 @@ import { getThemeByName, theme } from "../modes/interactive/theme/theme.js";
24
24
  import { stripFrontmatter } from "../utils/frontmatter.js";
25
25
  import { resolvePath } from "../utils/paths.js";
26
26
  import { sleep } from "../utils/sleep.js";
27
+ import { AgentAbortProvenance } from "./agent-abort-provenance.js";
28
+ import { AgentSettledDelivery } from "./agent-settled-delivery.js";
27
29
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
28
30
  import { executeBashWithOperations } from "./bash-executor.js";
29
31
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
@@ -46,7 +48,7 @@ import { isBillingErrorMessage } from "./retry-fallback/billing.js";
46
48
  import { formatSelector } from "./retry-fallback/chains.js";
47
49
  import { RetryFallbackController } from "./retry-fallback/controller.js";
48
50
  import { SelectorCooldowns } from "./retry-fallback/cooldown.js";
49
- import { classifyRateLimitedWait, nextInTurnDelayMs, probeBackSchedule, } from "./retry-fallback/hint-policy.js";
51
+ import { classifyRateLimitedWait, degradeWithoutFallback, nextInTurnDelayMs, probeBackSchedule, } from "./retry-fallback/hint-policy.js";
50
52
  import { createFallbackLogger } from "./retry-fallback/log.js";
51
53
  import { ProbeBackScheduler } from "./retry-fallback/probe-scheduler.js";
52
54
  import { validateFallbackChains } from "./retry-fallback/validate.js";
@@ -109,7 +111,9 @@ function describeCompactionRejection(cause) {
109
111
  case "circuit-breaker":
110
112
  return "Compaction rejected: the compaction circuit breaker is open after repeated failures. Wait for the cooldown and retry.";
111
113
  case "per-turn-cap":
112
- return "Compaction rejected: per-turn compaction cap reached for this turn.";
114
+ // Historical cause identifier kept for extension-API stability; since the
115
+ // per-turn soft cap was removed it fires only at the absolute session cap.
116
+ return "Compaction rejected: absolute compaction cap reached for this session.";
113
117
  case "stale-revision":
114
118
  return "Compaction rejected: the session changed while the summary was being prepared. Retry compaction against the latest context.";
115
119
  }
@@ -245,8 +249,10 @@ export class AgentSession {
245
249
  this._retryPromise = undefined;
246
250
  this._retryResolve = undefined;
247
251
  this._userAbortPromise = undefined;
248
- this._agentAbortSource = undefined;
252
+ this._abortProvenance = new AgentAbortProvenance();
253
+ this._agentSettledDelivery = new AgentSettledDelivery();
249
254
  this._suppressQueuedContinuationAfterUserAbort = false;
255
+ this._userAbortGeneration = 0;
250
256
  /** Set when clearQueue({ abortWillFollow: true }) drains queues immediately before abort(). */
251
257
  this._hadClearedQueuedMessages = false;
252
258
  this._extensionEventSignal = undefined;
@@ -796,17 +802,27 @@ export class AgentSession {
796
802
  await this.agent.waitForIdle();
797
803
  }
798
804
  if (!this._isAgentRunActive) {
805
+ this._abortProvenance.closeAgentEndBoundary();
799
806
  this._resolveIdleWaitIfIdle();
800
807
  return;
801
808
  }
802
809
  this._isAgentRunActive = false;
810
+ let deferredActions = [];
811
+ this._agentSettledDelivery.begin(this._userAbortGeneration);
803
812
  try {
804
813
  await this._extensionRunner.emit({ type: "agent_settled" });
805
814
  this._emit({ type: "agent_settled" });
815
+ if (this._abortProvenance.takeLateUserJoin())
816
+ await this._emitSessionAbort();
817
+ deferredActions = this._agentSettledDelivery.finish(this._userAbortGeneration);
806
818
  }
807
819
  finally {
820
+ this._agentSettledDelivery.cancel();
821
+ this._abortProvenance.closeAgentEndBoundary();
808
822
  this._resolveIdleWaitIfIdle();
809
823
  }
824
+ for (const action of deferredActions)
825
+ action();
810
826
  }
811
827
  async _promptAgent(messages) {
812
828
  this._isAgentRunActive = true;
@@ -1084,8 +1100,14 @@ export class AgentSession {
1084
1100
  finally {
1085
1101
  this._extensionEventSignal = undefined;
1086
1102
  }
1103
+ if (event.type === "agent_end" && this._abortProvenance.takeLateUserJoin())
1104
+ await this._emitSessionAbort();
1087
1105
  // Notify all listeners
1088
1106
  this._emit(event.type === "agent_end" ? { ...event, willRetry: agentEndWillRetry } : event);
1107
+ if (event.type === "agent_end") {
1108
+ if (this._abortProvenance.takeLateUserJoin())
1109
+ await this._emitSessionAbort();
1110
+ }
1089
1111
  // Handle session persistence
1090
1112
  if (event.type === "message_end") {
1091
1113
  // Check if this is a custom message from extensions
@@ -1154,7 +1176,9 @@ export class AgentSession {
1154
1176
  // Retry transient failures normally and eligible hard errors only through a fallback.
1155
1177
  const retryableError = this._isRetryableError(msg);
1156
1178
  const hardErrorFallbackEligible = this._isHardErrorFallbackEligible(msg);
1157
- const retryCanAdmitProvider = this.settingsManager.getRetrySettings().enabled && (retryableError || hardErrorFallbackEligible);
1179
+ const retryCanAdmitProvider = !userAbortSuppressedQueuedContinuation &&
1180
+ this.settingsManager.getRetrySettings().enabled &&
1181
+ (retryableError || hardErrorFallbackEligible);
1158
1182
  let compactedBeforeRetry = false;
1159
1183
  if (retryCanAdmitProvider &&
1160
1184
  requiredAutoCompaction &&
@@ -1164,7 +1188,7 @@ export class AgentSession {
1164
1188
  retryContinuationBlocked = !compactedBeforeRetry;
1165
1189
  }
1166
1190
  let retryOutcome = "not-handled";
1167
- if (!retryContinuationBlocked) {
1191
+ if (!retryContinuationBlocked && !userAbortSuppressedQueuedContinuation) {
1168
1192
  if (retryableError) {
1169
1193
  retryOutcome = await this._handleRetryableError(msg);
1170
1194
  }
@@ -1172,11 +1196,13 @@ export class AgentSession {
1172
1196
  retryOutcome = await this._handleRetryableError(msg, { hardErrorFallback: true });
1173
1197
  }
1174
1198
  }
1175
- if (retryOutcome === "continued")
1199
+ if (retryOutcome === "continued") {
1200
+ this._abortProvenance.closeAgentEndBoundary();
1176
1201
  return;
1202
+ }
1177
1203
  this._resolveRetry();
1178
1204
  retryContinuationBlocked ||= retryOutcome === "blocked";
1179
- if (!retryContinuationBlocked) {
1205
+ if (!retryContinuationBlocked && !userAbortSuppressedQueuedContinuation) {
1180
1206
  if (compactedBeforeRetry && this.agent.hasQueuedMessages()) {
1181
1207
  // Accepted recovery supersedes the stored admission rejection: the
1182
1208
  // queued continuation is about to run, so the originating prompt must
@@ -1223,6 +1249,9 @@ export class AgentSession {
1223
1249
  if (!launchedContinuation) {
1224
1250
  await this._emitAgentSettled();
1225
1251
  }
1252
+ else {
1253
+ this._abortProvenance.closeAgentEndBoundary();
1254
+ }
1226
1255
  }
1227
1256
  }
1228
1257
  /** Resolve the pending retry promise */
@@ -1238,6 +1267,10 @@ export class AgentSession {
1238
1267
  this._hintDeadlineMs = undefined;
1239
1268
  this._cumulativeHintedWaitMs = 0;
1240
1269
  }
1270
+ async _emitSessionAbort() {
1271
+ await this._extensionRunner.emit({ type: "session_abort" });
1272
+ this._emit({ type: "session_abort" });
1273
+ }
1241
1274
  /**
1242
1275
  * Arm the probe-back scheduler for a tier-2 demoted selector. The scheduler
1243
1276
  * will fire at most two probes (half-hint, then deadline) and clear the
@@ -1377,19 +1410,12 @@ export class AgentSession {
1377
1410
  await this._extensionRunner.emit({ type: "agent_start" });
1378
1411
  }
1379
1412
  else if (event.type === "agent_end") {
1380
- const abortSource = this._agentAbortSource;
1381
- const aborted = abortSource !== undefined || this._findLastAssistantInMessages(event.messages)?.stopReason === "aborted";
1413
+ const extensionEvent = this._abortProvenance.beginAgentEnd(event.messages, agentEndWillRetry, this._findLastAssistantInMessages(event.messages)?.stopReason === "aborted");
1382
1414
  try {
1383
- await this._extensionRunner.emit({
1384
- type: "agent_end",
1385
- messages: event.messages,
1386
- willRetry: agentEndWillRetry,
1387
- ...(aborted ? { aborted: true } : {}),
1388
- ...(abortSource === undefined ? {} : { abortSource }),
1389
- });
1415
+ await this._extensionRunner.emit(extensionEvent);
1390
1416
  }
1391
1417
  finally {
1392
- this._agentAbortSource = undefined;
1418
+ this._abortProvenance.endAgentEnd(extensionEvent);
1393
1419
  }
1394
1420
  }
1395
1421
  else if (event.type === "turn_start") {
@@ -2406,6 +2432,7 @@ export class AgentSession {
2406
2432
  * @param options.deliverAs Delivery mode: "steer", "followUp", or "nextTurn"
2407
2433
  */
2408
2434
  async sendCustomMessage(message, options) {
2435
+ const userAbortGeneration = this._userAbortGeneration;
2409
2436
  const appMessage = {
2410
2437
  role: "custom",
2411
2438
  customType: message.customType,
@@ -2425,6 +2452,8 @@ export class AgentSession {
2425
2452
  try {
2426
2453
  if (waitForExistingSessionWork) {
2427
2454
  await this._waitForSettledSessionWork();
2455
+ if (userAbortGeneration !== this._userAbortGeneration)
2456
+ return;
2428
2457
  finishSessionWork = this._sessionWorkBarrier.begin();
2429
2458
  }
2430
2459
  if (options?.deliverAs === "nextTurn") {
@@ -2625,31 +2654,20 @@ export class AgentSession {
2625
2654
  * Abort current operation and wait for agent to become idle.
2626
2655
  */
2627
2656
  async abort() {
2628
- // Capture gap-state BEFORE _abortActiveAgentAndRetry resets retry/compaction state.
2629
- // A mid-run abort (actively streaming with no pending retry) is delivered via
2630
- // agent_end (abortSource "user") — no session_abort needed. The gap case is when
2631
- // no agent_end will fire to carry the abort signal:
2632
- // - retry backoff (_retryAbortController defined — the error agent_end already
2633
- // fired, agent.abort() during backoff is a no-op, no new agent_end)
2634
- // - compaction (!isStreaming && isCompacting)
2635
- // - queued continuation that was already cleared by the caller (TUI clears queues
2636
- // before calling abort, so pendingMessageCount is 0 but _hadClearedQueuedMessages
2637
- // records that messages were present)
2638
- // Purely-idle defensive aborts (e.g. RPC session close on an idle session) must not
2639
- // fire session_abort.
2657
+ // Streaming aborts are carried by agent_end provenance; only gaps need session_abort.
2640
2658
  const wasMidRun = this.isStreaming && this._retryAbortController === undefined;
2641
2659
  const hadRetryBackoff = this._retryAbortController !== undefined;
2642
2660
  const hadCompactionOrPending = !this.isStreaming && (this.isCompacting || this.pendingMessageCount > 0);
2643
2661
  const hadClearedQueues = this._hadClearedQueuedMessages;
2662
+ const joinedAgentEndBoundary = this._abortProvenance.hasOpenAgentEndBoundary;
2644
2663
  this._hadClearedQueuedMessages = false;
2645
- const shouldEmitAbort = !wasMidRun && (hadRetryBackoff || hadCompactionOrPending || hadClearedQueues);
2664
+ const shouldEmitAbort = !joinedAgentEndBoundary && !wasMidRun && (hadRetryBackoff || hadCompactionOrPending || hadClearedQueues);
2646
2665
  this.abortCompaction();
2647
2666
  await this._abortActiveAgentAndRetry("user");
2648
2667
  if (!shouldEmitAbort)
2649
2668
  return;
2650
2669
  try {
2651
- await this._extensionRunner.emit({ type: "session_abort" });
2652
- this._emit({ type: "session_abort" });
2670
+ await this._emitSessionAbort();
2653
2671
  }
2654
2672
  catch {
2655
2673
  // Extension runner may be torn down during RPC close — best-effort.
@@ -3038,18 +3056,32 @@ export class AgentSession {
3038
3056
  this._pendingCompactionAdmission = undefined;
3039
3057
  admission.finishSessionWork();
3040
3058
  }
3059
+ _recordUserAbort() {
3060
+ this._suppressQueuedContinuationAfterUserAbort = true;
3061
+ this._userAbortGeneration += 1;
3062
+ }
3041
3063
  async _abortActiveAgentAndRetry(source) {
3042
3064
  this.abortRetry();
3043
3065
  this.abortBranchSummary();
3066
+ if (this._userAbortPromise === undefined) {
3067
+ const boundaryJoin = this._abortProvenance.joinOpenBoundary(source);
3068
+ if (boundaryJoin !== undefined) {
3069
+ if (boundaryJoin.userOwned)
3070
+ this._recordUserAbort();
3071
+ return;
3072
+ }
3073
+ }
3044
3074
  if (this._userAbortPromise) {
3045
- this.agent.abort();
3075
+ const joined = this._abortProvenance.join(source, this.isStreaming);
3076
+ if (joined.userOwned)
3077
+ this._recordUserAbort();
3078
+ if (joined.abortCurrentAgent)
3079
+ this.agent.abort();
3046
3080
  await this._userAbortPromise;
3047
3081
  return;
3048
3082
  }
3049
- if (this.isStreaming) {
3050
- this._suppressQueuedContinuationAfterUserAbort = true;
3051
- this._agentAbortSource = source;
3052
- }
3083
+ if (this.isStreaming && this._abortProvenance.begin(source))
3084
+ this._recordUserAbort();
3053
3085
  const abortPromise = (async () => {
3054
3086
  this.agent.abort();
3055
3087
  await this.waitForIdle();
@@ -4178,13 +4210,16 @@ export class AgentSession {
4178
4210
  };
4179
4211
  runner.bindCore({
4180
4212
  sendMessage: (message, options) => {
4181
- this.sendCustomMessage(message, options).catch((err) => {
4213
+ const send = () => this.sendCustomMessage(message, options).catch((err) => {
4182
4214
  runner.emitError({
4183
4215
  extensionPath: RUNTIME_EXTENSION_PATH,
4184
4216
  event: "send_message",
4185
4217
  error: err instanceof Error ? err.message : String(err),
4186
4218
  });
4187
4219
  });
4220
+ if (this._agentSettledDelivery.defer(send))
4221
+ return;
4222
+ send();
4188
4223
  },
4189
4224
  sendUserMessage: (content, options) => {
4190
4225
  this.sendUserMessage(content, options).catch((err) => {
@@ -4247,11 +4282,11 @@ export class AgentSession {
4247
4282
  getAgentDir: () => this._agentDir,
4248
4283
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
4249
4284
  getSignal: () => this._extensionEventSignal ?? this.agent.signal,
4250
- abort: () => {
4251
- if (this._extensionAbortHandler) {
4252
- this._extensionAbortHandler();
4253
- return;
4254
- }
4285
+ abort: (source = "user") => {
4286
+ if (source === "system")
4287
+ return void this._abortActiveAgentAndRetry("system");
4288
+ if (this._extensionAbortHandler)
4289
+ return this._extensionAbortHandler();
4255
4290
  void this.abort();
4256
4291
  },
4257
4292
  hasPendingMessages: () => this.pendingMessageCount > 0,
@@ -4663,6 +4698,41 @@ export class AgentSession {
4663
4698
  },
4664
4699
  };
4665
4700
  }
4701
+ /**
4702
+ * A 429-class failure with no usable fallback candidate must not fail the
4703
+ * turn with zero attempts: a provider answering 429 is asking for a retry.
4704
+ * No-hint and tier2 waits degrade to same-model in-turn retries under the
4705
+ * normal retry budget (tier2 clamps the hinted wait to the in-turn cap);
4706
+ * only tier3 hour-plus waits stay terminal, with the requested wait named
4707
+ * in the final error. Returns the in-turn retry delay, or undefined after
4708
+ * emitting the terminal auto_retry_end.
4709
+ */
4710
+ _degradeRateLimitedWithoutFallback(tier, hintMs, message, errorMessage) {
4711
+ const settings = this.settingsManager.getRetrySettings();
4712
+ const hintSettings = this.settingsManager.getHintPolicySettings();
4713
+ const finishTurn = (attempt, finalError) => {
4714
+ const exhaustedChainKey = this._retryFallback.exhaustedChainKey;
4715
+ if (exhaustedChainKey) {
4716
+ this._emit({ type: "retry_fallback_exhausted", chainKey: exhaustedChainKey, lastError: errorMessage });
4717
+ }
4718
+ this._emit({ type: "auto_retry_end", success: false, attempt, finalError });
4719
+ this._retryAttempt = 0;
4720
+ this._resetHintTierState();
4721
+ this._resolveRetry();
4722
+ };
4723
+ const degraded = degradeWithoutFallback(tier, hintMs, this._retryAttempt + 1, settings.baseDelayMs, hintSettings.hintedWaitCapMs);
4724
+ if (degraded.kind === "fail") {
4725
+ const waitSeconds = Math.ceil(degraded.hintMs / 1000);
4726
+ finishTurn(this._retryAttempt, `Provider requested a ${waitSeconds}s wait before retrying and no usable fallback model is available. ${message.errorMessage ?? ""}`);
4727
+ return undefined;
4728
+ }
4729
+ this._retryAttempt++;
4730
+ if (this._retryAttempt > settings.maxRetries) {
4731
+ finishTurn(this._retryAttempt - 1, message.errorMessage);
4732
+ return undefined;
4733
+ }
4734
+ return degraded.delayMs;
4735
+ }
4666
4736
  /**
4667
4737
  * Handle retryable errors with exponential backoff.
4668
4738
  * @returns whether retry continuation started, was blocked by compaction, or was not handled
@@ -4760,30 +4830,17 @@ export class AgentSession {
4760
4830
  const tier = classifyRateLimitedWait(hintMs, hintSettings);
4761
4831
  is429TierRouted = true;
4762
4832
  if (tier === "no-hint-fast-fallback") {
4763
- // Skip same-model retries entirely; fall back immediately.
4833
+ // Fall back immediately when a candidate exists; otherwise degrade
4834
+ // to same-model in-turn retries instead of failing the turn.
4764
4835
  switchedFallback = await this._retryFallback.tryFallback("transient", { errorMessage });
4765
4836
  if (switchedFallback) {
4766
4837
  this._retryAttempt = 1;
4767
4838
  }
4768
4839
  else {
4769
- const exhaustedChainKey = this._retryFallback.exhaustedChainKey;
4770
- if (exhaustedChainKey) {
4771
- this._emit({
4772
- type: "retry_fallback_exhausted",
4773
- chainKey: exhaustedChainKey,
4774
- lastError: errorMessage,
4775
- });
4776
- }
4777
- this._emit({
4778
- type: "auto_retry_end",
4779
- success: false,
4780
- attempt: 0,
4781
- finalError: message.errorMessage,
4782
- });
4783
- this._retryAttempt = 0;
4784
- this._resetHintTierState();
4785
- this._resolveRetry();
4786
- return "not-handled";
4840
+ const degradedDelayMs = this._degradeRateLimitedWithoutFallback(tier, hintMs, message, errorMessage);
4841
+ if (degradedDelayMs === undefined)
4842
+ return "not-handled";
4843
+ hintTierDelayMs = degradedDelayMs;
4787
4844
  }
4788
4845
  }
4789
4846
  else if (tier === "tier1-in-turn") {
@@ -4879,24 +4936,10 @@ export class AgentSession {
4879
4936
  }
4880
4937
  }
4881
4938
  else {
4882
- const exhaustedChainKey = this._retryFallback.exhaustedChainKey;
4883
- if (exhaustedChainKey) {
4884
- this._emit({
4885
- type: "retry_fallback_exhausted",
4886
- chainKey: exhaustedChainKey,
4887
- lastError: errorMessage,
4888
- });
4889
- }
4890
- this._emit({
4891
- type: "auto_retry_end",
4892
- success: false,
4893
- attempt: 0,
4894
- finalError: message.errorMessage,
4895
- });
4896
- this._retryAttempt = 0;
4897
- this._resetHintTierState();
4898
- this._resolveRetry();
4899
- return "not-handled";
4939
+ const degradedDelayMs = this._degradeRateLimitedWithoutFallback(tier, hintMs, message, errorMessage);
4940
+ if (degradedDelayMs === undefined)
4941
+ return "not-handled";
4942
+ hintTierDelayMs = degradedDelayMs;
4900
4943
  }
4901
4944
  }
4902
4945
  }