@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.
- package/CHANGELOG.md +29 -0
- package/dist/bundle/chunks/{anthropic-messages-3CFTFCKV.js → anthropic-messages-EMTBHJMM.js} +2 -2
- package/dist/bundle/chunks/{args-SJWMIH2H.js → args-LWR7VFKS.js} +1 -1
- package/dist/bundle/chunks/{chunk-IRKZ6FVC.js → chunk-35M5MVUZ.js} +149 -145
- package/dist/bundle/chunks/chunk-3DNYAO3P.js +2 -0
- package/dist/bundle/chunks/{chunk-T2BEB5SO.js → chunk-7QPFFAJT.js} +1 -1
- package/dist/bundle/chunks/{chunk-4RUVN7K3.js → chunk-BOCN744P.js} +1 -1
- package/dist/bundle/chunks/{chunk-DDHBG2HU.js → chunk-FM7NDZR5.js} +1 -1
- package/dist/bundle/chunks/{chunk-OMY4EC25.js → chunk-FSENE7JK.js} +2 -2
- package/dist/bundle/chunks/{chunk-Z2LCTXS2.js → chunk-G7LLAQXI.js} +3 -3
- package/dist/bundle/chunks/{chunk-NRHOGI4Z.js → chunk-IM6ZWYTW.js} +2 -2
- package/dist/bundle/chunks/{chunk-MLKYJIUK.js → chunk-KM4FMQII.js} +1 -1
- package/dist/bundle/chunks/{chunk-OOWML2KE.js → chunk-PMGEQYEP.js} +2 -2
- package/dist/bundle/chunks/chunk-QGJLZ6BI.js +2 -0
- package/dist/bundle/chunks/{chunk-6EXDMO4S.js → chunk-QJ6MUFXY.js} +1 -1
- package/dist/bundle/chunks/{chunk-FZTO2ICI.js → chunk-QRGAMVEE.js} +1 -1
- package/dist/bundle/chunks/{chunk-3QVP3ZVI.js → chunk-QXEWAKXA.js} +1 -1
- package/dist/bundle/chunks/{chunk-UUMKAA7S.js → chunk-RL5QMGQD.js} +9 -9
- package/dist/bundle/chunks/chunk-T36LJ6MW.js +3 -0
- package/dist/bundle/chunks/{chunk-GAHIHXVE.js → chunk-VEKNDMUA.js} +1 -1
- package/dist/bundle/chunks/{chunk-TFXUF2IL.js → chunk-VPV7ADF3.js} +1 -1
- package/dist/bundle/chunks/{chunk-RELAITQ7.js → chunk-X6TWPBI6.js} +1 -1
- package/dist/bundle/chunks/{chunk-PKDWFVE4.js → chunk-YUMRZIKJ.js} +6 -6
- package/dist/bundle/chunks/chunk-ZPSJQHDK.js +395 -0
- package/dist/bundle/chunks/{cli-main-JCSB54VT.js → cli-main-EABMHWFV.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-FVPGRSYA.js → help-fast-path-BYWJN6YX.js} +1 -1
- package/dist/bundle/chunks/{host-command-X2ZZFTME.js → host-command-5T34JLBP.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-OKWTQFUJ.js → host-lifecycle-GSRIQP6Z.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/{host-session-command-HKJKOAVS.js → host-session-command-LF6JODPX.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-GJERAFGV.js → interactive-mode-GMWHBIUC.js} +1 -1
- package/dist/bundle/chunks/{list-tips-AWDAUHF2.js → list-tips-VSWED3PT.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-JWMGLKAG.js → multi-session-host-3J7STKGD.js} +4 -4
- package/dist/bundle/chunks/{openai-codex-responses-RLYFSLK6.js → openai-codex-responses-7YOBPZ2N.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-OWNSXMTD.js → openai-completions-HSZPYXEA.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-FSVS2ND2.js → package-manager-cli-LWQZSWKL.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-M6QV2MMB.js → rotation-stream-QRRWL73X.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-TPG3J3ZJ.js → rpc-mode-6LSWRKXR.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-PAVLVMWL.js → schedule-command-7CXMW4WK.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-NSMRMKKK.js → session-control-endpoint-E265WAIQ.js} +1 -1
- package/dist/bundle/chunks/{session-picker-UV6IVWJY.js → session-picker-XRB3LP5W.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +298 -293
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-run-continuation.d.ts +24 -0
- package/dist/core/agent-run-continuation.js +57 -0
- package/dist/core/agent-session.d.ts +38 -1
- package/dist/core/agent-session.js +122 -13
- package/dist/core/extension-handler-stall-report.d.ts +11 -0
- package/dist/core/extension-handler-stall-report.js +26 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-command.js +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.d.ts +13 -1
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +15 -4
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +2 -0
- package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +2 -0
- package/dist/core/extensions/builtin/anthropic-subscription/failover.js +3 -2
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.d.ts +8 -0
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.js +29 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +2 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.js +4 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +4 -2
- package/dist/core/extensions/handler-budget.d.ts +84 -0
- package/dist/core/extensions/handler-budget.js +136 -0
- package/dist/core/extensions/runner.d.ts +23 -0
- package/dist/core/extensions/runner.js +184 -99
- package/dist/core/footer-data-provider.js +2 -1
- package/dist/core/keybindings.d.ts +1 -1
- package/dist/core/keybindings.js +3 -2
- package/dist/core/pending-input-journal.d.ts +6 -1
- package/dist/core/pending-input-journal.js +90 -34
- package/dist/core/pending-input-public.d.ts +11 -0
- package/dist/core/pending-input-public.js +10 -0
- package/dist/core/pending-input-session-port.d.ts +15 -0
- package/dist/core/pending-input-session-port.js +93 -0
- package/dist/core/pending-input-session.d.ts +108 -0
- package/dist/core/pending-input-session.js +220 -0
- package/dist/core/pending-input-store.d.ts +32 -1
- package/dist/core/pending-input-store.js +74 -26
- package/dist/core/pending-input-types.d.ts +25 -4
- package/dist/core/pending-input.js +36 -18
- package/dist/core/session-entry-file.js +1 -0
- package/dist/core/session-log.js +1 -1
- package/dist/core/session-manager.d.ts +1 -0
- package/dist/core/session-manager.js +5 -1
- package/dist/core/session-move/bundle.d.ts +39 -0
- package/dist/core/session-move/bundle.js +108 -0
- package/dist/core/session-move/export.d.ts +18 -0
- package/dist/core/session-move/export.js +83 -0
- package/dist/core/session-move/finish.d.ts +24 -0
- package/dist/core/session-move/finish.js +81 -0
- package/dist/core/session-move/import.d.ts +32 -0
- package/dist/core/session-move/import.js +124 -0
- package/dist/core/session-move/move-ledger.d.ts +47 -0
- package/dist/core/session-move/move-ledger.js +151 -0
- package/dist/core/session-move/ownership.d.ts +61 -0
- package/dist/core/session-move/ownership.js +89 -0
- package/dist/core/session-move/requeue.d.ts +8 -0
- package/dist/core/session-move/requeue.js +18 -0
- package/dist/core/session-move/write-guard.d.ts +22 -0
- package/dist/core/session-move/write-guard.js +44 -0
- package/dist/core/settings-manager.d.ts +13 -0
- package/dist/core/settings-manager.js +17 -0
- package/dist/core/turn-deadline.d.ts +2 -0
- package/dist/core/turn-deadline.js +10 -3
- package/dist/modes/interactive/components/ask-user-async-widget.d.ts +7 -0
- package/dist/modes/interactive/components/ask-user-async-widget.js +20 -6
- package/dist/modes/rpc/connection-handler.js +3 -0
- package/dist/modes/rpc/effect-commands.js +3 -4
- package/dist/modes/rpc/host-observe-request.js +1 -0
- package/dist/modes/rpc/rpc-client.d.ts +10 -0
- package/dist/modes/rpc/rpc-client.js +11 -0
- package/dist/modes/rpc/rpc-input-validation.js +8 -0
- package/dist/modes/rpc/rpc-types.d.ts +25 -1
- package/dist/modes/rpc/session-command-activity.js +1 -0
- package/dist/modes/rpc/session-control-actions-data.js +17 -8
- package/dist/modes/rpc/session-held.js +1 -0
- package/dist/modes/rpc/session-release.js +16 -1
- package/dist/modes/rpc/tool-child/admission.d.ts +42 -0
- package/dist/modes/rpc/tool-child/admission.js +81 -0
- package/dist/modes/rpc/tool-child/admission.test.d.ts +2 -0
- package/dist/modes/rpc/tool-child/admission.test.js +166 -0
- package/dist/modes/rpc/tool-child/codec.d.ts +21 -0
- package/dist/modes/rpc/tool-child/codec.js +137 -0
- package/dist/modes/rpc/tool-child/codec.test.d.ts +2 -0
- package/dist/modes/rpc/tool-child/codec.test.js +234 -0
- package/dist/modes/rpc/tool-child/context.d.ts +42 -0
- package/dist/modes/rpc/tool-child/context.js +23 -0
- package/dist/modes/rpc/tool-child/flow-control.d.ts +24 -0
- package/dist/modes/rpc/tool-child/flow-control.js +80 -0
- package/dist/modes/rpc/tool-child/payload-ref-paths.d.ts +9 -0
- package/dist/modes/rpc/tool-child/payload-ref-paths.js +45 -0
- package/dist/modes/rpc/tool-child/payload-ref.d.ts +58 -0
- package/dist/modes/rpc/tool-child/payload-ref.js +166 -0
- package/dist/modes/rpc/tool-child/payload-ref.test.d.ts +2 -0
- package/dist/modes/rpc/tool-child/payload-ref.test.js +254 -0
- package/dist/modes/rpc/tool-child/payload-wire.d.ts +13 -0
- package/dist/modes/rpc/tool-child/payload-wire.js +22 -0
- package/dist/modes/rpc/tool-child/protocol-guard.d.ts +9 -0
- package/dist/modes/rpc/tool-child/protocol-guard.js +128 -0
- package/dist/modes/rpc/tool-child/protocol.d.ts +192 -0
- package/dist/modes/rpc/tool-child/protocol.js +54 -0
- package/dist/utils/wsl.d.ts +2 -2
- package/dist/utils/wsl.js +4 -4
- package/docs/providers.md +2 -0
- package/docs/rpc.md +16 -2
- package/docs/settings.md +1 -0
- package/docs/terminal-setup.md +2 -0
- package/package.json +10 -6
- package/dist/bundle/chunks/chunk-COJK5FDH.js +0 -2
- package/dist/bundle/chunks/chunk-IDXW4FAO.js +0 -2
- package/dist/bundle/chunks/chunk-PFNBY43W.js +0 -2
- 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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 <
|
|
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 <
|
|
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
|
|
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);
|