@code-yeongyu/senpi 2026.10.18-1 → 2026.10.19

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 (154) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/bundle/chunks/{anthropic-messages-3CFTFCKV.js → anthropic-messages-EMTBHJMM.js} +2 -2
  3. package/dist/bundle/chunks/{args-SJWMIH2H.js → args-LWR7VFKS.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-IRKZ6FVC.js → chunk-35M5MVUZ.js} +149 -145
  5. package/dist/bundle/chunks/chunk-3DNYAO3P.js +2 -0
  6. package/dist/bundle/chunks/{chunk-T2BEB5SO.js → chunk-7QPFFAJT.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-4RUVN7K3.js → chunk-BOCN744P.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-DDHBG2HU.js → chunk-FM7NDZR5.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-OMY4EC25.js → chunk-FSENE7JK.js} +2 -2
  10. package/dist/bundle/chunks/{chunk-Z2LCTXS2.js → chunk-G7LLAQXI.js} +3 -3
  11. package/dist/bundle/chunks/{chunk-NRHOGI4Z.js → chunk-IM6ZWYTW.js} +2 -2
  12. package/dist/bundle/chunks/{chunk-MLKYJIUK.js → chunk-KM4FMQII.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-OOWML2KE.js → chunk-PMGEQYEP.js} +2 -2
  14. package/dist/bundle/chunks/chunk-QGJLZ6BI.js +2 -0
  15. package/dist/bundle/chunks/{chunk-6EXDMO4S.js → chunk-QJ6MUFXY.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-FZTO2ICI.js → chunk-QRGAMVEE.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-3QVP3ZVI.js → chunk-QXEWAKXA.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-UUMKAA7S.js → chunk-RL5QMGQD.js} +9 -9
  19. package/dist/bundle/chunks/chunk-T36LJ6MW.js +3 -0
  20. package/dist/bundle/chunks/{chunk-GAHIHXVE.js → chunk-VEKNDMUA.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-TFXUF2IL.js → chunk-VPV7ADF3.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-RELAITQ7.js → chunk-X6TWPBI6.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-PKDWFVE4.js → chunk-YUMRZIKJ.js} +6 -6
  24. package/dist/bundle/chunks/chunk-ZPSJQHDK.js +395 -0
  25. package/dist/bundle/chunks/{cli-main-JCSB54VT.js → cli-main-EABMHWFV.js} +1 -1
  26. package/dist/bundle/chunks/{help-fast-path-FVPGRSYA.js → help-fast-path-BYWJN6YX.js} +1 -1
  27. package/dist/bundle/chunks/{host-command-X2ZZFTME.js → host-command-5T34JLBP.js} +1 -1
  28. package/dist/bundle/chunks/{host-lifecycle-OKWTQFUJ.js → host-lifecycle-GSRIQP6Z.js} +1 -1
  29. package/dist/bundle/chunks/host-lifecycle.js +1 -1
  30. package/dist/bundle/chunks/{host-session-command-HKJKOAVS.js → host-session-command-LF6JODPX.js} +1 -1
  31. package/dist/bundle/chunks/{interactive-mode-GJERAFGV.js → interactive-mode-GMWHBIUC.js} +1 -1
  32. package/dist/bundle/chunks/{list-tips-AWDAUHF2.js → list-tips-VSWED3PT.js} +1 -1
  33. package/dist/bundle/chunks/{multi-session-host-JWMGLKAG.js → multi-session-host-3J7STKGD.js} +4 -4
  34. package/dist/bundle/chunks/{openai-codex-responses-RLYFSLK6.js → openai-codex-responses-7YOBPZ2N.js} +1 -1
  35. package/dist/bundle/chunks/{openai-completions-OWNSXMTD.js → openai-completions-HSZPYXEA.js} +1 -1
  36. package/dist/bundle/chunks/{package-manager-cli-FSVS2ND2.js → package-manager-cli-LWQZSWKL.js} +1 -1
  37. package/dist/bundle/chunks/{rotation-stream-M6QV2MMB.js → rotation-stream-QRRWL73X.js} +1 -1
  38. package/dist/bundle/chunks/{rpc-mode-TPG3J3ZJ.js → rpc-mode-6LSWRKXR.js} +1 -1
  39. package/dist/bundle/chunks/{schedule-command-PAVLVMWL.js → schedule-command-7CXMW4WK.js} +1 -1
  40. package/dist/bundle/chunks/{session-control-endpoint-NSMRMKKK.js → session-control-endpoint-E265WAIQ.js} +1 -1
  41. package/dist/bundle/chunks/{session-picker-UV6IVWJY.js → session-picker-XRB3LP5W.js} +1 -1
  42. package/dist/bundle/chunks/session-worker.js +298 -293
  43. package/dist/bundle/cli.js +2 -2
  44. package/dist/bundle/index.js +1 -1
  45. package/dist/bundle/rpc-entry.js +1 -1
  46. package/dist/bundle/runtime-manifest.json +1 -1
  47. package/dist/core/agent-run-continuation.d.ts +24 -0
  48. package/dist/core/agent-run-continuation.js +57 -0
  49. package/dist/core/agent-session.d.ts +38 -1
  50. package/dist/core/agent-session.js +122 -13
  51. package/dist/core/extension-handler-stall-report.d.ts +11 -0
  52. package/dist/core/extension-handler-stall-report.js +26 -0
  53. package/dist/core/extensions/builtin/anthropic-subscription/account-command.js +1 -0
  54. package/dist/core/extensions/builtin/anthropic-subscription/affinity.d.ts +13 -1
  55. package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +15 -4
  56. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +2 -0
  57. package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +2 -0
  58. package/dist/core/extensions/builtin/anthropic-subscription/failover.js +3 -2
  59. package/dist/core/extensions/builtin/anthropic-subscription/guidance.d.ts +8 -0
  60. package/dist/core/extensions/builtin/anthropic-subscription/guidance.js +29 -0
  61. package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +2 -0
  62. package/dist/core/extensions/builtin/anthropic-subscription/settings.js +4 -0
  63. package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +4 -2
  64. package/dist/core/extensions/handler-budget.d.ts +84 -0
  65. package/dist/core/extensions/handler-budget.js +136 -0
  66. package/dist/core/extensions/runner.d.ts +23 -0
  67. package/dist/core/extensions/runner.js +184 -99
  68. package/dist/core/footer-data-provider.js +2 -1
  69. package/dist/core/keybindings.d.ts +1 -1
  70. package/dist/core/keybindings.js +3 -2
  71. package/dist/core/pending-input-journal.d.ts +6 -1
  72. package/dist/core/pending-input-journal.js +90 -34
  73. package/dist/core/pending-input-public.d.ts +11 -0
  74. package/dist/core/pending-input-public.js +10 -0
  75. package/dist/core/pending-input-session-port.d.ts +15 -0
  76. package/dist/core/pending-input-session-port.js +93 -0
  77. package/dist/core/pending-input-session.d.ts +108 -0
  78. package/dist/core/pending-input-session.js +220 -0
  79. package/dist/core/pending-input-store.d.ts +32 -1
  80. package/dist/core/pending-input-store.js +74 -26
  81. package/dist/core/pending-input-types.d.ts +25 -4
  82. package/dist/core/pending-input.js +36 -18
  83. package/dist/core/session-entry-file.js +1 -0
  84. package/dist/core/session-log.js +1 -1
  85. package/dist/core/session-manager.d.ts +1 -0
  86. package/dist/core/session-manager.js +5 -1
  87. package/dist/core/session-move/bundle.d.ts +39 -0
  88. package/dist/core/session-move/bundle.js +108 -0
  89. package/dist/core/session-move/export.d.ts +18 -0
  90. package/dist/core/session-move/export.js +83 -0
  91. package/dist/core/session-move/finish.d.ts +24 -0
  92. package/dist/core/session-move/finish.js +81 -0
  93. package/dist/core/session-move/import.d.ts +32 -0
  94. package/dist/core/session-move/import.js +124 -0
  95. package/dist/core/session-move/move-ledger.d.ts +47 -0
  96. package/dist/core/session-move/move-ledger.js +151 -0
  97. package/dist/core/session-move/ownership.d.ts +61 -0
  98. package/dist/core/session-move/ownership.js +89 -0
  99. package/dist/core/session-move/requeue.d.ts +8 -0
  100. package/dist/core/session-move/requeue.js +18 -0
  101. package/dist/core/session-move/write-guard.d.ts +22 -0
  102. package/dist/core/session-move/write-guard.js +44 -0
  103. package/dist/core/settings-manager.d.ts +13 -0
  104. package/dist/core/settings-manager.js +17 -0
  105. package/dist/core/turn-deadline.d.ts +2 -0
  106. package/dist/core/turn-deadline.js +10 -3
  107. package/dist/modes/interactive/components/ask-user-async-widget.d.ts +7 -0
  108. package/dist/modes/interactive/components/ask-user-async-widget.js +20 -6
  109. package/dist/modes/rpc/connection-handler.js +3 -0
  110. package/dist/modes/rpc/effect-commands.js +3 -4
  111. package/dist/modes/rpc/host-observe-request.js +1 -0
  112. package/dist/modes/rpc/rpc-client.d.ts +10 -0
  113. package/dist/modes/rpc/rpc-client.js +11 -0
  114. package/dist/modes/rpc/rpc-input-validation.js +8 -0
  115. package/dist/modes/rpc/rpc-types.d.ts +25 -1
  116. package/dist/modes/rpc/session-command-activity.js +1 -0
  117. package/dist/modes/rpc/session-control-actions-data.js +17 -8
  118. package/dist/modes/rpc/session-held.js +1 -0
  119. package/dist/modes/rpc/session-release.js +16 -1
  120. package/dist/modes/rpc/tool-child/admission.d.ts +42 -0
  121. package/dist/modes/rpc/tool-child/admission.js +81 -0
  122. package/dist/modes/rpc/tool-child/admission.test.d.ts +2 -0
  123. package/dist/modes/rpc/tool-child/admission.test.js +166 -0
  124. package/dist/modes/rpc/tool-child/codec.d.ts +21 -0
  125. package/dist/modes/rpc/tool-child/codec.js +137 -0
  126. package/dist/modes/rpc/tool-child/codec.test.d.ts +2 -0
  127. package/dist/modes/rpc/tool-child/codec.test.js +234 -0
  128. package/dist/modes/rpc/tool-child/context.d.ts +42 -0
  129. package/dist/modes/rpc/tool-child/context.js +23 -0
  130. package/dist/modes/rpc/tool-child/flow-control.d.ts +24 -0
  131. package/dist/modes/rpc/tool-child/flow-control.js +80 -0
  132. package/dist/modes/rpc/tool-child/payload-ref-paths.d.ts +9 -0
  133. package/dist/modes/rpc/tool-child/payload-ref-paths.js +45 -0
  134. package/dist/modes/rpc/tool-child/payload-ref.d.ts +58 -0
  135. package/dist/modes/rpc/tool-child/payload-ref.js +166 -0
  136. package/dist/modes/rpc/tool-child/payload-ref.test.d.ts +2 -0
  137. package/dist/modes/rpc/tool-child/payload-ref.test.js +254 -0
  138. package/dist/modes/rpc/tool-child/payload-wire.d.ts +13 -0
  139. package/dist/modes/rpc/tool-child/payload-wire.js +22 -0
  140. package/dist/modes/rpc/tool-child/protocol-guard.d.ts +9 -0
  141. package/dist/modes/rpc/tool-child/protocol-guard.js +128 -0
  142. package/dist/modes/rpc/tool-child/protocol.d.ts +192 -0
  143. package/dist/modes/rpc/tool-child/protocol.js +54 -0
  144. package/dist/utils/wsl.d.ts +2 -2
  145. package/dist/utils/wsl.js +4 -4
  146. package/docs/providers.md +2 -0
  147. package/docs/rpc.md +16 -2
  148. package/docs/settings.md +1 -0
  149. package/docs/terminal-setup.md +2 -0
  150. package/package.json +10 -6
  151. package/dist/bundle/chunks/chunk-COJK5FDH.js +0 -2
  152. package/dist/bundle/chunks/chunk-IDXW4FAO.js +0 -2
  153. package/dist/bundle/chunks/chunk-PFNBY43W.js +0 -2
  154. package/dist/bundle/chunks/chunk-QBD4LY7D.js +0 -395
@@ -33,6 +33,7 @@ import { sleep } from "../utils/sleep.js";
33
33
  import { normalizeToolResultImages } from "../utils/tool-result-images.js";
34
34
  import { AgentAbortProvenance } from "./agent-abort-provenance.js";
35
35
  import { AgentBusyError } from "./agent-busy-error.js";
36
+ import { AgentRunContinuations } from "./agent-run-continuation.js";
36
37
  import { AgentSettledDelivery, DeferredTurnClaim } from "./agent-settled-delivery.js";
37
38
  import { resolveAssistantUsageScope } from "./assistant-usage-scope.js";
38
39
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
@@ -56,6 +57,7 @@ import { environmentContextMessageIfChanged, resolveEnvironmentContext, } from "
56
57
  import { areExperimentalFeaturesEnabled } from "./experimental.js";
57
58
  import { exportSessionToHtml } from "./export-html/index.js";
58
59
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
60
+ import { reportExtensionHandlerStall } from "./extension-handler-stall-report.js";
59
61
  import { ANTHROPIC_SUBSCRIPTION_PROVIDER_ID } from "./extensions/builtin/anthropic-subscription/account-management.js";
60
62
  import { askUserAnswerDisplayText } from "./extensions/builtin/ask-user/format.js";
61
63
  import { getPromptCachePrewarmUsage } from "./extensions/builtin/cache-keepalive/prewarm-entry.js";
@@ -85,6 +87,7 @@ import { ModelRegistry } from "./model-registry.js";
85
87
  import { getModelNarrowingPatterns, resolveModelScope } from "./model-resolver.js";
86
88
  import { ModelSwitchAuthError } from "./model-switch-auth-error.js";
87
89
  import { NestedToolCallRunner } from "./nested-tool-calls.js";
90
+ import { SessionPendingInputs } from "./pending-input-session.js";
88
91
  import { PROMPT_CACHE_SAFE_WAIT_ENV, resolvePromptCacheSafeWaitSeconds } from "./prompt-cache-budget.js";
89
92
  import { PromptCachePrefixBuilds } from "./prompt-cache-prefix-request.js";
90
93
  import { expandPromptTemplateWithMetadata } from "./prompt-templates.js";
@@ -122,7 +125,7 @@ import { composeFilesystemPolicies } from "./tools/filesystem-policy.js";
122
125
  import { createAllToolDefinitions } from "./tools/index.js";
123
126
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
124
127
  import { TranscriptWriteFailures } from "./transcript-write-failures.js";
125
- import { AgentDeadlineTracker, resolveTurnDeadlineSettings, TURN_DEADLINE_ENTRY_TYPE, } from "./turn-deadline.js";
128
+ import { AgentDeadlineTracker, resolveTurnDeadlineSettings, TURN_DEADLINE_ENTRY_TYPE, TURN_DEADLINE_GRACE_MS, } from "./turn-deadline.js";
126
129
  import { commandShapedName, findUnknownCommand } from "./unknown-command.js";
127
130
  import { addUsageToTotals, combineUsage, createUsageTotals } from "./usage-totals.js";
128
131
  import { keepsTextBlocksVerbatim, userTextContent } from "./user-text-blocks.js";
@@ -318,6 +321,10 @@ export class AgentSession {
318
321
  /** Settles once the queued message_end processing (persistence included) of that exact message has run. */
319
322
  this._messageEndPersistence = new WeakMap();
320
323
  this._isAgentRunActive = false;
324
+ /** senpi#3650: the core may continue a run after agent_end; the settle must not wait on that continuation. */
325
+ this._runContinuations = new AgentRunContinuations();
326
+ /** senpi#3650: the turn that owns extension handler waits started now; the turn deadline releases only its own. */
327
+ this._handlerTurnScope = 0;
321
328
  this._toolExecutionDepth = 0;
322
329
  this._toolContextDisposers = new Set();
323
330
  this._promptStartPending = false;
@@ -341,6 +348,7 @@ export class AgentSession {
341
348
  this._queuedInputOrder = [];
342
349
  this._nextQueuedInputOrder = 0;
343
350
  this._nextQueuedId = 0;
351
+ this._queueClearRecorded = Promise.resolve();
344
352
  /** The exact message object queued for each `queuedId`, so removal and edits hit the native queue by identity. */
345
353
  this._queuedMessages = new Map();
346
354
  /** Recently delivered `queuedId`s (bounded), so a late dequeue answers `already_delivered`, not `not_found`. */
@@ -465,6 +473,7 @@ export class AgentSession {
465
473
  this._lastAssistantMessage = undefined;
466
474
  /** Internal handler for agent events - shared by subscribe and reconnect */
467
475
  this._handleAgentEvent = (event, signal) => {
476
+ this._runContinuations.observe(event);
468
477
  // Record the calls a tool made through ctx.executeTool() and their usage on its result message,
469
478
  // synchronously so the message carries them before any listener or persistence sees it.
470
479
  if (this._nestedToolCalls) {
@@ -540,6 +549,12 @@ export class AgentSession {
540
549
  this._browserEngine = config.browserEngine;
541
550
  this.agent = config.agent;
542
551
  this.sessionManager = config.sessionManager;
552
+ this._pendingInputs = new SessionPendingInputs({
553
+ sessionManager: this.sessionManager,
554
+ nextQueuedId: () => this._mintQueuedId(),
555
+ onChange: (update) => this._emit({ type: "pending_input_update", ...update }),
556
+ });
557
+ this.agent.queuedInputPort = this._pendingInputs.queuedInputPort();
543
558
  this.settingsManager = config.settingsManager;
544
559
  this._unsubscribeSettingsSource = this.settingsManager.subscribeToSourceSelection((source) => {
545
560
  this._emit({ type: "settings_source_selected", ...source });
@@ -1706,9 +1721,12 @@ export class AgentSession {
1706
1721
  this._resolveIdleWait = undefined;
1707
1722
  resolve();
1708
1723
  }
1709
- async _emitAgentSettled() {
1710
- if (this.agent.state.isStreaming) {
1711
- await this.agent.waitForIdle();
1724
+ async _emitAgentSettled(agentEnd) {
1725
+ if (this.agent.state.isStreaming &&
1726
+ (await this._runContinuations.idleOrContinued(agentEnd, () => this.agent.waitForIdle()))) {
1727
+ // The core continued this run with input queued during agent_end; that continuation settles it.
1728
+ this._abortProvenance.closeAgentEndBoundary();
1729
+ return;
1712
1730
  }
1713
1731
  if (!this._isAgentRunActive) {
1714
1732
  this._abortProvenance.closeAgentEndBoundary();
@@ -1816,6 +1834,7 @@ export class AgentSession {
1816
1834
  this._isAgentRunActive = true;
1817
1835
  // A new turn admitted: arm the inactivity lease and the hard max-runtime cap.
1818
1836
  this._deadlineTracker.arm();
1837
+ this._handlerTurnScope += 1;
1819
1838
  // A new prompt replaces any retry that compaction before it scheduled.
1820
1839
  this._failedResponse = undefined;
1821
1840
  this._recordSelection();
@@ -2473,7 +2492,7 @@ export class AgentSession {
2473
2492
  launchedContinuation = true;
2474
2493
  }
2475
2494
  if (!launchedContinuation) {
2476
- await this._emitAgentSettled();
2495
+ await this._emitAgentSettled(event);
2477
2496
  }
2478
2497
  else {
2479
2498
  this._abortProvenance.closeAgentEndBoundary();
@@ -3086,6 +3105,29 @@ export class AgentSession {
3086
3105
  /** Adapter for the deadline owner interface: end the turn through the normal system abort. */
3087
3106
  async abortTurnForDeadline() {
3088
3107
  await this._abortActiveAgentAndRetry("system");
3108
+ // An abort that joined an open agent_end boundary returns at once; the run is still held until it settles.
3109
+ if (!this.isIdle)
3110
+ await this._waitForIdleAfterAbort();
3111
+ }
3112
+ /**
3113
+ * An abort (Esc, or the turn deadline) that the run does not honour within the grace is held by an extension handler
3114
+ * no abort signal reaches: release the turn's held handler waits so it settles (senpi#3650).
3115
+ */
3116
+ async _waitForIdleAfterAbort() {
3117
+ const idle = this.waitForIdle();
3118
+ let graceTimer;
3119
+ const graceOver = new Promise((resolve) => {
3120
+ graceTimer = setTimeout(() => resolve(true), TURN_DEADLINE_GRACE_MS);
3121
+ });
3122
+ const stillHeld = await Promise.race([idle.then(() => false), graceOver]);
3123
+ clearTimeout(graceTimer);
3124
+ if (stillHeld)
3125
+ this._extensionRunner.releaseHeldHandlers(this._handlerTurnScope);
3126
+ await idle;
3127
+ }
3128
+ /** Adapter for the deadline owner interface: stop waiting on extension handlers that hold the turn (senpi#3650). */
3129
+ releaseHeldExtensionHandlers() {
3130
+ return this._extensionRunner.releaseHeldHandlers(this._handlerTurnScope);
3089
3131
  }
3090
3132
  /** Record a deadline stop durably so the goal engine holds off until admitted user input. */
3091
3133
  _recordDeadlineStop(deadline) {
@@ -4398,17 +4440,32 @@ export class AgentSession {
4398
4440
  * Internal: Queue a steering message (already expanded, no extension command check).
4399
4441
  */
4400
4442
  async _queueSteer(text, images, options) {
4401
- this._enqueuePreparedInput({ ...options, text, images, mode: "steer" });
4443
+ await this._enqueueQueuedInput({ ...options, text, images, mode: "steer" });
4402
4444
  }
4403
4445
  /**
4404
4446
  * Internal: Queue a follow-up message (already expanded, no extension command check).
4405
4447
  */
4406
4448
  async _queueFollowUp(text, images, options) {
4407
- this._enqueuePreparedInput({ ...options, text, images, mode: "followUp" });
4449
+ await this._enqueueQueuedInput({ ...options, text, images, mode: "followUp" });
4450
+ }
4451
+ /** Identified input is admitted into durable custody and enqueued in one gated step; other input as before. */
4452
+ async _enqueueQueuedInput(input) {
4453
+ const clientMessageId = input.clientMessageId;
4454
+ if (clientMessageId === undefined) {
4455
+ this._enqueuePreparedInput(input);
4456
+ return;
4457
+ }
4458
+ await this._pendingInputs.admit({ clientMessageId, text: input.text, ...(input.images ? { images: input.images } : {}), mode: input.mode }, (queuedId) => this._enqueuePreparedInput({ ...input, queuedId }));
4408
4459
  }
4409
4460
  /** Restore accepted input without re-running input transforms or changing its enqueue order. */
4410
4461
  async restoreQueuedInput(input) {
4411
- this._enqueuePreparedInput(input);
4462
+ const restored = input.clientMessageId === undefined
4463
+ ? undefined
4464
+ : await this._pendingInputs.restoreHandle(input.clientMessageId);
4465
+ // The pending-input record is the authority: an input it already retired is not queued again.
4466
+ if (restored === "retired")
4467
+ return;
4468
+ this._enqueuePreparedInput(restored === undefined ? input : { ...input, queuedId: restored.queuedId });
4412
4469
  }
4413
4470
  _enqueuePreparedInput(input) {
4414
4471
  const enqueueOrder = input.enqueueOrder ?? this.reserveQueuedInputOrder();
@@ -4422,7 +4479,7 @@ export class AgentSession {
4422
4479
  input.onQueuedInput?.(prepared);
4423
4480
  const queue = input.mode === "steer" ? this._steeringMessages : this._followUpMessages;
4424
4481
  queue.push(input.text);
4425
- const queuedId = this._recordQueuedInput(input.text, input.mode, prepared);
4482
+ const queuedId = this._recordQueuedInput(input.text, input.mode, prepared, undefined, input.queuedId);
4426
4483
  this._sessionLogger.debug("queue_enqueue", {
4427
4484
  mode: input.mode,
4428
4485
  count: queue.length,
@@ -4820,11 +4877,10 @@ export class AgentSession {
4820
4877
  this._nextQueuedInputOrder += 1;
4821
4878
  return this._nextQueuedInputOrder;
4822
4879
  }
4823
- _recordQueuedInput(text, mode, options, message) {
4880
+ _recordQueuedInput(text, mode, options, message, admittedQueuedId) {
4824
4881
  const order = options?.enqueueOrder ?? this.reserveQueuedInputOrder();
4825
4882
  this._nextQueuedInputOrder = Math.max(this._nextQueuedInputOrder, order);
4826
- this._nextQueuedId += 1;
4827
- const queuedId = `q_${this._nextQueuedId}`;
4883
+ const queuedId = admittedQueuedId ?? this._mintQueuedId();
4828
4884
  this._queuedInputOrder.push({
4829
4885
  text,
4830
4886
  mode,
@@ -4837,6 +4893,10 @@ export class AgentSession {
4837
4893
  this._queuedMessages.set(queuedId, message);
4838
4894
  return queuedId;
4839
4895
  }
4896
+ _mintQueuedId() {
4897
+ this._nextQueuedId += 1;
4898
+ return `q_${this._nextQueuedId}`;
4899
+ }
4840
4900
  /** A queued input reached a turn: forget its record and remember its id as delivered. */
4841
4901
  _removeQueuedInput(text, mode, clientMessageId) {
4842
4902
  const index = this._queuedInputOrder.findIndex((message) => clientMessageId === undefined
@@ -4866,6 +4926,23 @@ export class AgentSession {
4866
4926
  editQueuedInput(queuedId, text, options) {
4867
4927
  return this._changeQueuedInput(queuedId, text, options?.expectedRevision);
4868
4928
  }
4929
+ /**
4930
+ * Removes (`text` undefined) or edits one queued input. An input in durable custody (admitted with a client id)
4931
+ * changes in its record first and in the native queue in the same gated step; any other input changes as
4932
+ * `dequeueQueuedInput` / `editQueuedInput` do.
4933
+ */
4934
+ async changeQueuedInput(queuedId, text, options) {
4935
+ const clientMessageId = this._queuedInputOrder.find((input) => input.queuedId === queuedId)?.clientMessageId;
4936
+ const durable = clientMessageId === undefined
4937
+ ? undefined
4938
+ : await this._pendingInputs.change({
4939
+ clientMessageId,
4940
+ text,
4941
+ ...(options?.expectedRevision === undefined ? {} : { expectedRevision: options.expectedRevision }),
4942
+ ...(options?.effectId === undefined ? {} : { effectId: options.effectId }),
4943
+ }, () => this._changeQueuedInput(queuedId, text, undefined));
4944
+ return durable ?? this._changeQueuedInput(queuedId, text, options?.expectedRevision);
4945
+ }
4869
4946
  // Synchronous from the revision check to the change: nothing else on the session loop can land in between.
4870
4947
  _changeQueuedInput(queuedId, nextText, expectedRevision) {
4871
4948
  const index = this._queuedInputOrder.findIndex((input) => input.queuedId === queuedId);
@@ -4950,6 +5027,14 @@ export class AgentSession {
4950
5027
  this._followUpMessages = [];
4951
5028
  this._queuedInputOrder = [];
4952
5029
  this._queuedMessages.clear();
5030
+ const recorded = this._pendingInputs.recordCleared(ordered.flatMap((input) => (input.clientMessageId === undefined ? [] : [input.clientMessageId])));
5031
+ // A caller that acknowledges the clear awaits `recorded` and sees a failure; this logs it when nobody does.
5032
+ recorded.catch((error) => {
5033
+ this._sessionLogger.warn("pending_input_clear_unrecorded", {
5034
+ error: error instanceof Error ? error.message : String(error),
5035
+ });
5036
+ });
5037
+ this._queueClearRecorded = recorded;
4953
5038
  this._postCompactionDeferredSteeringMessages = [];
4954
5039
  this._postCompactionDeferredFollowUpMessages = [];
4955
5040
  this.agent.clearAllQueues();
@@ -4962,6 +5047,13 @@ export class AgentSession {
4962
5047
  });
4963
5048
  return cleared;
4964
5049
  }
5050
+ /**
5051
+ * Settles once the last `clearQueue` is recorded in the pending-input journal. `clearQueue` stays synchronous;
5052
+ * a caller that acknowledges the clear (RPC `clear_queue`) awaits this first so its answer is durable.
5053
+ */
5054
+ whenQueueClearRecorded() {
5055
+ return this._queueClearRecorded;
5056
+ }
4965
5057
  /** Number of pending messages (includes both steering and follow-up) */
4966
5058
  get pendingMessageCount() {
4967
5059
  return this._steeringMessages.length + this._followUpMessages.length;
@@ -4974,6 +5066,10 @@ export class AgentSession {
4974
5066
  getFollowUpMessages() {
4975
5067
  return this._followUpMessages;
4976
5068
  }
5069
+ /** The pending inputs in durable custody, and an explicit outcome for each id the caller already knows. */
5070
+ getPendingInputs(knownClientMessageIds) {
5071
+ return this._pendingInputs.snapshot(knownClientMessageIds);
5072
+ }
4977
5073
  getQueuedInputs() {
4978
5074
  return [...this._queuedInputOrder].sort((left, right) => left.enqueueOrder - right.enqueueOrder);
4979
5075
  }
@@ -5006,6 +5102,15 @@ export class AgentSession {
5006
5102
  if (this._isBeforeSettle)
5007
5103
  this._abortDuringBeforeSettle = true;
5008
5104
  await this._abortActiveAgentAndRetry("user");
5105
+ // An Esc that joined an open agent_end boundary returns at once: a boundary handler that holds the run is still
5106
+ // released after the grace (senpi#3650), without making abort() itself wait for it.
5107
+ if (!this.isIdle) {
5108
+ this._waitForIdleAfterAbort().catch((error) => {
5109
+ this._sessionLogger.warn("abort_release_failed", {
5110
+ error: error instanceof Error ? error.message : String(error),
5111
+ });
5112
+ });
5113
+ }
5009
5114
  if (!shouldEmitAbort)
5010
5115
  return;
5011
5116
  try {
@@ -6023,7 +6128,7 @@ export class AgentSession {
6023
6128
  this._recordUserAbort();
6024
6129
  const abortPromise = (async () => {
6025
6130
  this.agent.abort();
6026
- await this.waitForIdle();
6131
+ await this._waitForIdleAfterAbort();
6027
6132
  })();
6028
6133
  this._userAbortPromise = abortPromise;
6029
6134
  try {
@@ -7582,6 +7687,10 @@ export class AgentSession {
7582
7687
  runner.setToolHookLifecycleObserver((event) => {
7583
7688
  this._emit(event);
7584
7689
  });
7690
+ runner.setEventHandlerBudget(() => ({
7691
+ warnMs: this.settingsManager.getExtensionEventHandlerWarnMs(),
7692
+ timeoutMs: this.settingsManager.getExtensionEventHandlerTimeoutMs(),
7693
+ }), (stall) => reportExtensionHandlerStall(stall, this._sessionLogger, (message, type) => runner.getUIContext().notify(message, type)), () => this._handlerTurnScope);
7585
7694
  runner.bindCommandContext(this._extensionCommandContextActions);
7586
7695
  this._extensionErrorUnsubscriber?.();
7587
7696
  this._extensionErrorUnsubscriber = this._extensionErrorListener
@@ -0,0 +1,11 @@
1
+ import type { ExtensionHandlerStall } from "./extensions/handler-budget.ts";
2
+ import type { SessionLogger } from "./session-log.ts";
3
+ type Notify = (message: string, type: "warning") => void;
4
+ /**
5
+ * Makes a stalled extension handler diagnosable (senpi#3650): every stage lands in session.log with the event and
6
+ * extension, and a handler still running past the warning is shown, because the turn is waiting on it. A timeout or
7
+ * deadline release is shown by the runner's extension error, so it is only logged here.
8
+ */
9
+ export declare function reportExtensionHandlerStall(stall: ExtensionHandlerStall, logger: SessionLogger, notify: Notify): void;
10
+ export {};
11
+ //# sourceMappingURL=extension-handler-stall-report.d.ts.map
@@ -0,0 +1,26 @@
1
+ import { basename } from "node:path";
2
+ function seconds(ms) {
3
+ return `${Math.max(1, Math.round(ms / 1000))} s`;
4
+ }
5
+ /**
6
+ * Makes a stalled extension handler diagnosable (senpi#3650): every stage lands in session.log with the event and
7
+ * extension, and a handler still running past the warning is shown, because the turn is waiting on it. A timeout or
8
+ * deadline release is shown by the runner's extension error, so it is only logged here.
9
+ */
10
+ export function reportExtensionHandlerStall(stall, logger, notify) {
11
+ const extension = basename(stall.extensionPath);
12
+ logger.warn("extension_handler_stalled", {
13
+ extension,
14
+ handlerEvent: stall.event,
15
+ stage: stall.stage,
16
+ durationMs: stall.elapsedMs,
17
+ ...(stall.error === undefined ? {} : { error: stall.error }),
18
+ });
19
+ if (stall.stage !== "warn")
20
+ return;
21
+ const cap = stall.timeoutMs > 0
22
+ ? ` The session continues without it after ${seconds(stall.timeoutMs)}.`
23
+ : " Action: set extensionEventHandlerTimeoutMs to bound it, or remove the extension.";
24
+ notify(`Extension ${extension} is still running its ${stall.event} handler after ${seconds(stall.elapsedMs)}, so this turn is waiting on it.${cap}`, "warning");
25
+ }
26
+ //# sourceMappingURL=extension-handler-stall-report.js.map
@@ -112,6 +112,7 @@ function showAccounts(ctx, settings, environment) {
112
112
  affinityPick = selectAccount(accounts, {
113
113
  sessionId: ctx.sessionManager.getSessionId(),
114
114
  pinnedAccount: pinned,
115
+ accountFallback: settings.accountFallback,
115
116
  ...(credential?.order === undefined ? {} : { accountOrder: credential.order }),
116
117
  }).name;
117
118
  }
@@ -9,12 +9,22 @@ export type AffinityOptions = {
9
9
  * session does not move accounts and re-send its whole conversation (senpi#2891).
10
10
  */
11
11
  preferredAccount?: string;
12
+ /** When false, a blocked pin, resident account, or HRW winner is never skipped. */
13
+ readonly accountFallback?: boolean;
12
14
  /** The user's account order (senpi#3591): tried after the pin and the session's own account, before HRW. */
13
15
  accountOrder?: readonly string[];
14
16
  now?: number;
15
17
  /** The requested model; an account blocked only for another model stays eligible for it. */
16
18
  model?: string;
17
19
  };
20
+ /**
21
+ * The one account a request may use when account fallback is off (senpi#3465): its name, and whether it
22
+ * is still configured. A pin or a session's resident account can name an account that was later removed.
23
+ */
24
+ export type UnavailableAccount = {
25
+ readonly name: string;
26
+ readonly configured: boolean;
27
+ };
18
28
  export declare class AllAccountsBlockedError extends Error {
19
29
  readonly soonestUnblockAt: number | undefined;
20
30
  /**
@@ -29,7 +39,9 @@ export declare class AllAccountsBlockedError extends Error {
29
39
  * chain moves only this model and keeps the provider for the others.
30
40
  */
31
41
  readonly limitedModel: string | undefined;
32
- constructor(soonestUnblockAt: number | undefined, blockReason?: "auth_error", limitedModel?: string);
42
+ /** Set only when account fallback is off, so the guidance names that account instead of the whole pool. */
43
+ readonly unavailableAccount: UnavailableAccount | undefined;
44
+ constructor(soonestUnblockAt: number | undefined, blockReason?: "auth_error", limitedModel?: string, unavailableAccount?: UnavailableAccount);
33
45
  }
34
46
  export declare function getAffinityKey(options: Pick<AffinityOptions, "affinityKey" | "sessionId">): string;
35
47
  /**
@@ -3,7 +3,7 @@ import { applyAccountOrder } from "@earendil-works/pi-ai/auth/pool/select";
3
3
  import { activeModelBlockUntil, pruneModelBlocks } from "../../../credential-pool/model-scope.js";
4
4
  export const DEFAULT_AFFINITY_KEY = "claude-sdk-oauth-default";
5
5
  export class AllAccountsBlockedError extends Error {
6
- constructor(soonestUnblockAt, blockReason, limitedModel) {
6
+ constructor(soonestUnblockAt, blockReason, limitedModel, unavailableAccount) {
7
7
  const until = soonestUnblockAt === undefined ? undefined : new Date(soonestUnblockAt).toISOString();
8
8
  super(limitedModel !== undefined && until !== undefined
9
9
  ? `All Anthropic Subscription accounts have hit the usage limit for model ${limitedModel} until ${until}.`
@@ -14,6 +14,7 @@ export class AllAccountsBlockedError extends Error {
14
14
  this.name = "AllAccountsBlockedError";
15
15
  this.soonestUnblockAt = soonestUnblockAt;
16
16
  this.blockReason = blockReason;
17
+ this.unavailableAccount = unavailableAccount;
17
18
  }
18
19
  }
19
20
  export function getAffinityKey(options) {
@@ -91,15 +92,25 @@ function limitedModel(accounts, now, model) {
91
92
  /** Selects a pinned or HRW-ranked account with no provider-global selection state. */
92
93
  export function selectAccount(accounts, options = {}) {
93
94
  const now = options.now ?? Date.now();
94
- const selected = selectUnblocked(accounts, options, now);
95
+ let candidates = accounts;
96
+ let unavailableAccount;
97
+ if (options.accountFallback === false) {
98
+ const name = options.pinnedAccount ??
99
+ options.preferredAccount ??
100
+ applyAccountOrder(options.accountOrder, rendezvousOrder(getAffinityKey(options), accounts))[0]?.name;
101
+ candidates = accounts.filter((account) => account.name === name);
102
+ if (name !== undefined)
103
+ unavailableAccount = { name, configured: candidates.length > 0 };
104
+ }
105
+ const selected = selectUnblocked(candidates, options, now);
95
106
  if (selected)
96
107
  return selected;
97
108
  // A stale persisted rate-limit entry must not dead-end the pool. Retry once
98
109
  // against the cleared view before reporting the earliest available account.
99
- const cleared = clearExpiredBlocks(accounts, now);
110
+ const cleared = clearExpiredBlocks(candidates, now);
100
111
  const afterClear = selectUnblocked(cleared, options, now);
101
112
  if (afterClear)
102
113
  return afterClear;
103
- throw new AllAccountsBlockedError(soonestUnblockAt(accounts, now, options.model), accounts.some((account) => account.blockReason === "auth_error") ? "auth_error" : undefined, limitedModel(accounts, now, options.model));
114
+ throw new AllAccountsBlockedError(soonestUnblockAt(candidates, now, options.model), candidates.some((account) => account.blockReason === "auth_error") ? "auth_error" : undefined, limitedModel(candidates, now, options.model), unavailableAccount);
104
115
  }
105
116
  //# sourceMappingURL=affinity.js.map
@@ -214,9 +214,11 @@ export async function* queryWithAuthLane(input) {
214
214
  });
215
215
  yield* runFailover({
216
216
  accounts: pool.accounts,
217
+ accountFallback: input.providerSettings.accountFallback,
217
218
  selectFn: (accounts) => selectAccount(accounts, {
218
219
  sessionId: input.sessionId,
219
220
  pinnedAccount: input.pinnedAccount ?? pool.pinnedAccount,
221
+ accountFallback: input.providerSettings.accountFallback,
220
222
  ...(pool.accountOrder === undefined ? {} : { accountOrder: pool.accountOrder }),
221
223
  ...(input.preferredAccount === undefined ? {} : { preferredAccount: input.preferredAccount }),
222
224
  now: activeBoundary.now(),
@@ -16,6 +16,8 @@ export type FailoverEvent = {
16
16
  };
17
17
  export type FailoverOptions<TEvent> = {
18
18
  accounts: readonly AccountSlot[];
19
+ /** When false, only the selected account may be attempted; same-account retries remain enabled. */
20
+ readonly accountFallback?: boolean;
19
21
  selectFn: (accounts: readonly AccountSlot[]) => AccountSlot;
20
22
  runAttempt: (slot: AccountSlot) => AsyncIterable<TEvent> | Promise<AsyncIterable<TEvent>>;
21
23
  classify: (error: unknown) => SdkErrorClassification;
@@ -273,12 +273,13 @@ export async function* runFailover(options) {
273
273
  const now = options.now ?? Date.now;
274
274
  const baseBlockMs = options.baseBlockMs ?? DEFAULT_RATE_LIMIT_BLOCK_MS;
275
275
  let accounts = clearExpiredBlocks(withSessionBlocks(options.accounts, options.sessionKey, now()), now());
276
+ const attemptLimit = options.accountFallback === false ? Math.min(accounts.length, 1) : accounts.length;
276
277
  let lastError;
277
278
  const retriedOnStoredMaterial = new Set();
278
279
  let transientRetriesUsed = 0;
279
280
  let longContextRetried = false;
280
281
  let retryAccountName;
281
- for (let attempt = 0; attempt < accounts.length; attempt++) {
282
+ for (let attempt = 0; attempt < attemptLimit; attempt++) {
282
283
  // Each attempt gets its own copy: prepareSlot refreshes the slot in place, and a concurrent
283
284
  // attempt holding the same stored object must still report the token it actually sent.
284
285
  const retrySlot = retryAccountName === undefined ? undefined : accounts.find((slot) => slot.name === retryAccountName);
@@ -361,7 +362,7 @@ export async function* runFailover(options) {
361
362
  visibleDeltaEmitted,
362
363
  };
363
364
  try {
364
- if (!visibleDeltaEmitted && attempt + 1 < accounts.length) {
365
+ if (!visibleDeltaEmitted && attempt + 1 < attemptLimit) {
365
366
  event.nextAccount = options.selectFn(accounts);
366
367
  }
367
368
  }
@@ -1,7 +1,15 @@
1
+ import type { UnavailableAccount } from "./affinity.ts";
1
2
  import type { SdkErrorKind } from "./errors.ts";
2
3
  import type { ClaudeCodeRun } from "./executable.ts";
3
4
  export declare function noAccountGuidance(hasAnthropicCredential: boolean): string;
4
5
  export declare function allAccountsBlockedGuidance(soonestUnblockAt: number | undefined, blockReason?: "auth_error"): string;
6
+ /**
7
+ * With account fallback off, only one account may serve (senpi#3465), so the pool-wide advice is wrong:
8
+ * other accounts may be healthy, and adding one does not help because it is never used. Name the account
9
+ * and the remedies that do work. The cause words ("authentication error", "rate limit") stay, so the outer
10
+ * credential-pool classifier reads the same verdict as for the pool-wide guidance.
11
+ */
12
+ export declare function singleAccountUnavailableGuidance(account: UnavailableAccount, soonestUnblockAt: number | undefined, blockReason?: "auth_error"): string;
5
13
  export declare function claudeCodeVersionFloorGuidance(text: string, ran?: ClaudeCodeRun): string | undefined;
6
14
  export declare function sdkErrorGuidance(kind: SdkErrorKind): string | undefined;
7
15
  export declare function missingBinaryGuidance(platform: string, arch: string): string;
@@ -35,6 +35,35 @@ export function allAccountsBlockedGuidance(soonestUnblockAt, blockReason) {
35
35
  ` /login ${PROVIDER} - add another account`,
36
36
  ].join("\n");
37
37
  }
38
+ const ENABLE_ACCOUNT_FALLBACK = " Set anthropicSubscriptionProvider.accountFallback: true (or SENPI_CLAUDE_SDK_OAUTH_ACCOUNT_FALLBACK=1) to switch accounts automatically.";
39
+ /**
40
+ * With account fallback off, only one account may serve (senpi#3465), so the pool-wide advice is wrong:
41
+ * other accounts may be healthy, and adding one does not help because it is never used. Name the account
42
+ * and the remedies that do work. The cause words ("authentication error", "rate limit") stay, so the outer
43
+ * credential-pool classifier reads the same verdict as for the pool-wide guidance.
44
+ */
45
+ export function singleAccountUnavailableGuidance(account, soonestUnblockAt, blockReason) {
46
+ if (!account.configured) {
47
+ return [
48
+ `Claude account '${account.name}' for ${PROVIDER} is not configured, and account fallback is off, so no other account is used.`,
49
+ ` /claude-account list - see the configured accounts`,
50
+ ` /claude-account pin <name> - pin a configured account (or /claude-account unpin)`,
51
+ " Start a new session if this session's account was removed.",
52
+ ENABLE_ACCOUNT_FALLBACK,
53
+ ].join("\n");
54
+ }
55
+ const until = soonestUnblockAt !== undefined && Number.isFinite(soonestUnblockAt)
56
+ ? new Date(soonestUnblockAt).toISOString()
57
+ : undefined;
58
+ const cause = blockReason === "auth_error" ? "authentication error" : "rate limit";
59
+ return [
60
+ `Claude account '${account.name}' for ${PROVIDER} is blocked (${cause})${until === undefined ? "" : ` until ${until}`}, and account fallback is off, so no other account is tried.`,
61
+ ...(until === undefined ? [] : [` Wait until ${until}, when this account can serve again.`]),
62
+ ...(blockReason === "auth_error" ? [` /login ${PROVIDER} - re-authenticate this account`] : []),
63
+ ` /claude-account pin <name> - switch to another account`,
64
+ ENABLE_ACCOUNT_FALLBACK,
65
+ ].join("\n");
66
+ }
38
67
  /**
39
68
  * The remedy for a version-floor 400 depends on which binary ran: updating senpi/omo only helps
40
69
  * the bundled one, and `claude update` (which the API text suggests) only helps a `claude` on PATH.
@@ -23,6 +23,8 @@ export interface AnthropicSubscriptionProviderSettings {
23
23
  readonly settingSources?: SettingSource[];
24
24
  readonly strictMcpConfig?: boolean;
25
25
  readonly pinnedAccount?: string;
26
+ /** Allow automatic switching to another managed account after failure. Defaults to true. */
27
+ readonly accountFallback?: boolean;
26
28
  readonly tokenInjection?: AnthropicSubscriptionTokenInjection;
27
29
  readonly compactionOwner?: AnthropicSubscriptionCompactionOwner;
28
30
  }
@@ -69,6 +69,7 @@ function parseProviderSettings(value) {
69
69
  const settingSources = parseSettingSources(value.settingSources);
70
70
  const strictMcpConfig = typeof value.strictMcpConfig === "boolean" ? value.strictMcpConfig : undefined;
71
71
  const pinnedAccount = parseNonEmptyString(value.pinnedAccount);
72
+ const accountFallback = typeof value.accountFallback === "boolean" ? value.accountFallback : undefined;
72
73
  const tokenInjection = parseTokenInjection(value.tokenInjection);
73
74
  const compactionOwner = parseCompactionOwner(value.compactionOwner);
74
75
  return {
@@ -80,6 +81,7 @@ function parseProviderSettings(value) {
80
81
  ...(settingSources !== undefined ? { settingSources } : {}),
81
82
  ...(strictMcpConfig !== undefined ? { strictMcpConfig } : {}),
82
83
  ...(pinnedAccount !== undefined ? { pinnedAccount } : {}),
84
+ ...(accountFallback !== undefined ? { accountFallback } : {}),
83
85
  ...(tokenInjection !== undefined ? { tokenInjection } : {}),
84
86
  ...(compactionOwner !== undefined ? { compactionOwner } : {}),
85
87
  };
@@ -92,6 +94,7 @@ function parseEnvironmentSettings(environment) {
92
94
  const tokenInjection = parseTokenInjection(environment.SENPI_CLAUDE_SDK_OAUTH_TOKEN_INJECTION);
93
95
  const settingSources = parseEnvironmentSettingSources(environment.SENPI_CLAUDE_SDK_OAUTH_SETTING_SOURCES);
94
96
  const pinnedAccount = parseNonEmptyString(environment.SENPI_CLAUDE_SDK_OAUTH_PINNED_ACCOUNT);
97
+ const accountFallback = parseEnvironmentBoolean(environment.SENPI_CLAUDE_SDK_OAUTH_ACCOUNT_FALLBACK);
95
98
  const compactionOwner = parseCompactionOwner(environment.SENPI_CLAUDE_SDK_OAUTH_COMPACTION_OWNER);
96
99
  return {
97
100
  ...(enabled !== undefined ? { enabled } : {}),
@@ -101,6 +104,7 @@ function parseEnvironmentSettings(environment) {
101
104
  ...(tokenInjection !== undefined ? { tokenInjection } : {}),
102
105
  ...(settingSources !== undefined ? { settingSources } : {}),
103
106
  ...(pinnedAccount !== undefined ? { pinnedAccount } : {}),
107
+ ...(accountFallback !== undefined ? { accountFallback } : {}),
104
108
  ...(compactionOwner !== undefined ? { compactionOwner } : {}),
105
109
  };
106
110
  }
@@ -1,9 +1,11 @@
1
1
  import { AllAccountsBlockedError } from "./affinity.js";
2
2
  import { classifySdkError } from "./errors.js";
3
- import { allAccountsBlockedGuidance, claudeCodeVersionFloorGuidance, sdkErrorGuidance } from "./guidance.js";
3
+ import { allAccountsBlockedGuidance, claudeCodeVersionFloorGuidance, sdkErrorGuidance, singleAccountUnavailableGuidance, } from "./guidance.js";
4
4
  export function withAuthGuidance(error, message, ran) {
5
5
  if (error instanceof AllAccountsBlockedError)
6
- return allAccountsBlockedGuidance(error.soonestUnblockAt, error.blockReason);
6
+ return error.unavailableAccount === undefined
7
+ ? allAccountsBlockedGuidance(error.soonestUnblockAt, error.blockReason)
8
+ : singleAccountUnavailableGuidance(error.unavailableAccount, error.soonestUnblockAt, error.blockReason);
7
9
  const guidance = sdkErrorGuidance(classifySdkError(error).kind);
8
10
  const versionGuidance = claudeCodeVersionFloorGuidance(message, ran);
9
11
  const hints = [guidance, versionGuidance].filter((hint) => hint !== undefined);