@skaile/workspaces 3.7.2 → 3.8.0
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 +146 -0
- package/dist/app-sdk/index.js +1 -1
- package/dist/{asset-feeds-44M2GV2W.js → asset-feeds-7HM3HONM.js} +4 -4
- package/dist/{asset-feeds-44M2GV2W.js.map → asset-feeds-7HM3HONM.js.map} +1 -1
- package/dist/asset-manager/index.js +1 -1
- package/dist/bridge/drivers/claude-sdk.js +231 -24
- package/dist/bridge/drivers/claude-sdk.js.map +1 -1
- package/dist/bridge/drivers/codex.js +1 -1
- package/dist/bridge/drivers/continue.js +2 -2
- package/dist/bridge/drivers/echo.js +1 -1
- package/dist/bridge/drivers/gemini.js +3 -3
- package/dist/bridge/drivers/goose.js +2 -2
- package/dist/bridge/drivers/omp.js +1 -1
- package/dist/bridge/drivers/opencode.js +2 -2
- package/dist/bridge/drivers/qwen.js +3 -3
- package/dist/bridge/index.js +2 -2
- package/dist/bridge/src/drivers/claude-sdk.d.ts +96 -1
- package/dist/bridge/src/drivers/claude-sdk.d.ts.map +1 -1
- package/dist/bridge/src/types.d.ts +103 -0
- package/dist/bridge/src/types.d.ts.map +1 -1
- package/dist/{chunk-IKNQW3AW.js → chunk-2GREL3OD.js} +3 -3
- package/dist/{chunk-IKNQW3AW.js.map → chunk-2GREL3OD.js.map} +1 -1
- package/dist/{chunk-Z7ILXHIT.js → chunk-2JW5SWIU.js} +6 -6
- package/dist/{chunk-Z7ILXHIT.js.map → chunk-2JW5SWIU.js.map} +1 -1
- package/dist/{chunk-J4I2YPGE.js → chunk-4WTC6BSV.js} +3 -3
- package/dist/{chunk-J4I2YPGE.js.map → chunk-4WTC6BSV.js.map} +1 -1
- package/dist/{chunk-JTBF4OB2.js → chunk-6GDVGAIZ.js} +587 -114
- package/dist/chunk-6GDVGAIZ.js.map +1 -0
- package/dist/{chunk-SID35UAK.js → chunk-7SHXLJQP.js} +3 -3
- package/dist/{chunk-SID35UAK.js.map → chunk-7SHXLJQP.js.map} +1 -1
- package/dist/{chunk-JF73DP5W.js → chunk-B7TA2XI7.js} +2 -2
- package/dist/{chunk-JF73DP5W.js.map → chunk-B7TA2XI7.js.map} +1 -1
- package/dist/{chunk-32TJ55W5.js → chunk-BGAAZMBK.js} +2 -2
- package/dist/chunk-BGAAZMBK.js.map +1 -0
- package/dist/{chunk-XCG63ZNJ.js → chunk-DGDELPOD.js} +3 -3
- package/dist/{chunk-XCG63ZNJ.js.map → chunk-DGDELPOD.js.map} +1 -1
- package/dist/{chunk-26L2B6P6.js → chunk-FUCYTGAE.js} +4 -4
- package/dist/chunk-FUCYTGAE.js.map +1 -0
- package/dist/{chunk-F7S6NDIW.js → chunk-IH2KJYAU.js} +142 -13
- package/dist/chunk-IH2KJYAU.js.map +1 -0
- package/dist/{chunk-VXITGB7G.js → chunk-KNPBMR2P.js} +3 -3
- package/dist/{chunk-VXITGB7G.js.map → chunk-KNPBMR2P.js.map} +1 -1
- package/dist/{chunk-BCXI27BH.js → chunk-NVYEK4FI.js} +3 -3
- package/dist/{chunk-BCXI27BH.js.map → chunk-NVYEK4FI.js.map} +1 -1
- package/dist/{chunk-D4VHBAJC.js → chunk-O3LEV6KB.js} +4 -4
- package/dist/{chunk-D4VHBAJC.js.map → chunk-O3LEV6KB.js.map} +1 -1
- package/dist/{chunk-2GA2UGQI.js → chunk-OZZCPTOG.js} +6 -6
- package/dist/{chunk-2GA2UGQI.js.map → chunk-OZZCPTOG.js.map} +1 -1
- package/dist/{chunk-NXHRSUKL.js → chunk-PRUCNSBI.js} +4 -4
- package/dist/{chunk-NXHRSUKL.js.map → chunk-PRUCNSBI.js.map} +1 -1
- package/dist/{chunk-J7SEUNPD.js → chunk-QVQH6GV2.js} +2 -2
- package/dist/{chunk-J7SEUNPD.js.map → chunk-QVQH6GV2.js.map} +1 -1
- package/dist/{chunk-I5TSXDTD.js → chunk-RVXBOKHK.js} +3 -3
- package/dist/{chunk-I5TSXDTD.js.map → chunk-RVXBOKHK.js.map} +1 -1
- package/dist/cli/index.js +30 -30
- package/dist/connectors/index.js +3 -3
- package/dist/connectors/src/connector-manager.d.ts +34 -2
- package/dist/connectors/src/connector-manager.d.ts.map +1 -1
- package/dist/connectors/src/connector-prompt.d.ts.map +1 -1
- package/dist/connectors/src/connector-types.d.ts +25 -0
- package/dist/connectors/src/connector-types.d.ts.map +1 -1
- package/dist/connectors/src/credential-helper-script.d.ts +48 -0
- package/dist/connectors/src/credential-helper-script.d.ts.map +1 -1
- package/dist/connectors/src/index.d.ts +3 -3
- package/dist/connectors/src/index.d.ts.map +1 -1
- package/dist/connectors/src/managed-gitconfig.d.ts +80 -0
- package/dist/connectors/src/managed-gitconfig.d.ts.map +1 -1
- package/dist/{connectors-DY7UBBV5.js → connectors-2OHRZDJZ.js} +3 -3
- package/dist/{connectors-DY7UBBV5.js.map → connectors-2OHRZDJZ.js.map} +1 -1
- package/dist/{ensure-sources-QRURV6JB.js → ensure-sources-42LTJ5QI.js} +4 -4
- package/dist/{ensure-sources-QRURV6JB.js.map → ensure-sources-42LTJ5QI.js.map} +1 -1
- package/dist/factory-assets/connectors/box.js +3 -3
- package/dist/factory-assets/connectors/deploy.js +3 -3
- package/dist/factory-assets/connectors/devserver.js +3 -3
- package/dist/factory-assets/connectors/flow/adapter.js +3 -3
- package/dist/factory-assets/connectors/flow/run-flow.js +4 -4
- package/dist/factory-assets/connectors/flow.js +3 -3
- package/dist/factory-assets/connectors/git/driver.d.ts +64 -1
- package/dist/factory-assets/connectors/git/driver.d.ts.map +1 -1
- package/dist/factory-assets/connectors/git.js +3 -3
- package/dist/factory-assets/connectors/gmail.js +3 -3
- package/dist/factory-assets/connectors/googledrive.js +3 -3
- package/dist/factory-assets/connectors/local.js +3 -3
- package/dist/factory-assets/connectors/mattermost.js +3 -3
- package/dist/factory-assets/connectors/memory.js +3 -3
- package/dist/factory-assets/connectors/minio.js +3 -3
- package/dist/factory-assets/connectors/postgres.js +3 -3
- package/dist/factory-assets/connectors/s3.js +3 -3
- package/dist/factory-assets/connectors/sharepoint.js +3 -3
- package/dist/factory-assets/connectors/sqlite.js +3 -3
- package/dist/factory-assets/connectors/static-server.js +3 -3
- package/dist/factory-assets/connectors/tunnel.js +3 -3
- package/dist/factory-assets/connectors/webdav.js +3 -3
- package/dist/factory-assets/connectors/xstate-store.js +3 -3
- package/dist/factory-assets/connectors/xstate.js +3 -3
- package/dist/{helpers-TLTVVZRR.js → helpers-BP7FX6CO.js} +4 -4
- package/dist/{helpers-TLTVVZRR.js.map → helpers-BP7FX6CO.js.map} +1 -1
- package/dist/{library-publish-6HXFLOXC.js → library-publish-X5Z32WJY.js} +6 -6
- package/dist/{library-publish-6HXFLOXC.js.map → library-publish-X5Z32WJY.js.map} +1 -1
- package/dist/{open-library-FMVJICL5.js → open-library-PHUMBYHP.js} +5 -5
- package/dist/{open-library-FMVJICL5.js.map → open-library-PHUMBYHP.js.map} +1 -1
- package/dist/{project-BG3N4I3V.js → project-HAMCXPNE.js} +5 -5
- package/dist/{project-BG3N4I3V.js.map → project-HAMCXPNE.js.map} +1 -1
- package/dist/runner/index.js +7 -7
- package/dist/runner/src/ai-provider-switch.d.ts +95 -0
- package/dist/runner/src/ai-provider-switch.d.ts.map +1 -0
- package/dist/runner/src/credential-rotated.d.ts +23 -4
- package/dist/runner/src/credential-rotated.d.ts.map +1 -1
- package/dist/runner/src/serve.d.ts.map +1 -1
- package/dist/runner/src/session-builder.d.ts +19 -0
- package/dist/runner/src/session-builder.d.ts.map +1 -1
- package/dist/sdk/asset-manager.js +1 -1
- package/dist/sdk/bridge.js +2 -2
- package/dist/sdk/index.js +7 -7
- package/dist/sdk/runner.js +7 -7
- package/dist/sdk/session.js +2 -2
- package/dist/sdk/types.js +1 -1
- package/dist/session/index.js +2 -2
- package/dist/{setup-O2N6PA7I.js → setup-IOOGR3EL.js} +4 -4
- package/dist/{setup-O2N6PA7I.js.map → setup-IOOGR3EL.js.map} +1 -1
- package/dist/{source-JDIBJZ3X.js → source-772GLNM6.js} +6 -6
- package/dist/{source-JDIBJZ3X.js.map → source-772GLNM6.js.map} +1 -1
- package/dist/{source-manifest-publish-567T25M6.js → source-manifest-publish-LAUGDQ5G.js} +7 -7
- package/dist/{source-manifest-publish-567T25M6.js.map → source-manifest-publish-LAUGDQ5G.js.map} +1 -1
- package/dist/tui/index.js +7 -7
- package/dist/types/index.js +1 -1
- package/dist/types/src/events.d.ts +34 -1
- package/dist/types/src/events.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/lifecycle.d.ts +13 -0
- package/dist/types/src/lifecycle.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +22 -1
- package/dist/types/src/version.d.ts.map +1 -1
- package/dist/workspace-plugin/adapters/mcp.js +2 -2
- package/dist/workspace-plugin/adapters/mcp.js.map +1 -1
- package/dist/workspace-plugin/adapters/omp.js +8 -8
- package/dist/workspace-plugin/adapters/omp.js.map +1 -1
- package/dist/workspace-plugin/index.js +1 -1
- package/dist/workspace-plugin/src/adapters/mcp.d.ts.map +1 -1
- package/dist/workspace-plugin/src/adapters/omp.d.ts.map +1 -1
- package/dist/workspace-plugin/src/tools/connectors.d.ts +7 -3
- package/dist/workspace-plugin/src/tools/connectors.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-26L2B6P6.js.map +0 -1
- package/dist/chunk-32TJ55W5.js.map +0 -1
- package/dist/chunk-F7S6NDIW.js.map +0 -1
- package/dist/chunk-JTBF4OB2.js.map +0 -1
|
@@ -2,7 +2,7 @@ import { scrubPoisonedTranscript, resolveClaudeConfigDir, SUPPORTED_CLOUDS, buil
|
|
|
2
2
|
import { dispatchCapability } from '../../chunk-RRVQAE5D.js';
|
|
3
3
|
import { fetchProviderModels } from '../../chunk-V34ZEXSE.js';
|
|
4
4
|
import { AuthError, classifyClaudeSdkError, reclassifyAuthAsAccountUnavailable, AccountUnavailableError } from '../../chunk-GAH4YRCI.js';
|
|
5
|
-
import { BUILTIN_DRIVER_CATALOG, AgentDriver, getBridgeLogger } from '../../chunk-
|
|
5
|
+
import { BUILTIN_DRIVER_CATALOG, AgentDriver, getBridgeLogger } from '../../chunk-BGAAZMBK.js';
|
|
6
6
|
import '../../chunk-6E6PKKAD.js';
|
|
7
7
|
import '../../chunk-AZPXCHDK.js';
|
|
8
8
|
import '../../chunk-F3HNW354.js';
|
|
@@ -159,7 +159,12 @@ if (typeof globalThis.require === "undefined") {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
function freshRetryBudget() {
|
|
162
|
-
return { staleResume: false, poison: false, auth: false, queryEnded: false };
|
|
162
|
+
return { staleResume: false, poison: false, auth: false, queryEnded: false, limit: false };
|
|
163
|
+
}
|
|
164
|
+
function isSeatCapacityFailure(detail, subtype) {
|
|
165
|
+
if (detail.category !== "rate_limit") return false;
|
|
166
|
+
if (subtype === "error_max_budget_usd") return false;
|
|
167
|
+
return detail.statusCode !== 529;
|
|
163
168
|
}
|
|
164
169
|
var QueryEndedBeforeTurnError = class extends Error {
|
|
165
170
|
constructor(message) {
|
|
@@ -216,6 +221,7 @@ var PushableInputStream = class {
|
|
|
216
221
|
}
|
|
217
222
|
};
|
|
218
223
|
var ABORT_GRACE_MS = 3e3;
|
|
224
|
+
var QUESTION_SUPERSEDED_BY_PROMPT = "The user replied in chat instead of answering this dialog, so the question was cancelled. Their reply arrives as the next user turn \u2014 read it before you re-ask or assume an answer.";
|
|
219
225
|
var claudeBinaryCache;
|
|
220
226
|
var claudeBinaryResolved = false;
|
|
221
227
|
var ClaudeSdkDriver = class extends AgentDriver {
|
|
@@ -291,6 +297,52 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
291
297
|
* reset elsewhere — the next call's set is the only legitimate transition.
|
|
292
298
|
*/
|
|
293
299
|
authSelfHealUsed = false;
|
|
300
|
+
/**
|
|
301
|
+
* Mirror of the in-flight turn's seat-failover budget — same contract as
|
|
302
|
+
* {@link authSelfHealUsed}, for the `onLimitBlocked` path.
|
|
303
|
+
*/
|
|
304
|
+
limitFailoverUsed = false;
|
|
305
|
+
/**
|
|
306
|
+
* Set when THIS attempt saw an upstream usage-limit rejection: either the
|
|
307
|
+
* SDK's `rate_limit_event` telemetry with `status: "rejected"`, or an error
|
|
308
|
+
* result the classifier calls `rate_limit`.
|
|
309
|
+
*
|
|
310
|
+
* The telemetry is the load-bearing half. A subscription ceiling arrives as
|
|
311
|
+
* `{"status":"rejected","type":"five_hour"}` followed by an error result
|
|
312
|
+
* whose text ("You've hit your session limit · resets 9:50am") matches none
|
|
313
|
+
* of the classifier's rate-limit keywords — so without this flag the
|
|
314
|
+
* rejection classifies as `unknown` and no failover is reachable at all.
|
|
315
|
+
*
|
|
316
|
+
* Reset at the top of every {@link runPrompt} attempt, so the replay after a
|
|
317
|
+
* failover observes its own outcome rather than inheriting the first one's.
|
|
318
|
+
*/
|
|
319
|
+
limitRejectionSeen = false;
|
|
320
|
+
/**
|
|
321
|
+
* True when {@link handleResultErrorPayload} rejected this turn ONLY so the
|
|
322
|
+
* seat-failover branch could run — a turn that, before this existed, would
|
|
323
|
+
* have emitted its error and resolved. If the failover then declines, the
|
|
324
|
+
* rejection is undone so a limit block with no peer available looks exactly
|
|
325
|
+
* as it always has downstream (one error event, one `agent_end`, a resolved
|
|
326
|
+
* promise) rather than also drawing the runner's fatal "prompt failed".
|
|
327
|
+
*/
|
|
328
|
+
limitTurnRejectedForFailover = false;
|
|
329
|
+
/**
|
|
330
|
+
* Whether this attempt already reported a provider rejection. One upstream
|
|
331
|
+
* failure routinely reaches the driver twice — `rejected` telemetry followed
|
|
332
|
+
* by the error result, or an `is_error` result followed by the iterator throw
|
|
333
|
+
* carrying the real text — and these observations feed counters, so counting
|
|
334
|
+
* one rejection twice would overstate the seat's pressure.
|
|
335
|
+
*/
|
|
336
|
+
providerResponseReported = false;
|
|
337
|
+
/** Limit-window metadata from this attempt's rejection telemetry, if any. */
|
|
338
|
+
lastLimitInfo = null;
|
|
339
|
+
/**
|
|
340
|
+
* The `agent-event: error` {@link failTurn} withheld because a recovery
|
|
341
|
+
* (auth self-heal or seat failover) was about to run. The recovery emits it
|
|
342
|
+
* if it declines, and drops it if it retries — so a recovered turn shows the
|
|
343
|
+
* user nothing and a failed one shows exactly one error.
|
|
344
|
+
*/
|
|
345
|
+
deferredTurnError = null;
|
|
294
346
|
/** Tracks whether a session has been started (for continue: true). */
|
|
295
347
|
hasSession = false;
|
|
296
348
|
/** Session ID from the SDK — used for streamInput messages. */
|
|
@@ -404,7 +456,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
404
456
|
* occurs after a container crash that prevented the SDK from flushing its session file.
|
|
405
457
|
*/
|
|
406
458
|
prompt(message) {
|
|
407
|
-
this.denyPendingQuestion(
|
|
459
|
+
this.denyPendingQuestion(QUESTION_SUPERSEDED_BY_PROMPT);
|
|
408
460
|
const queuedPrompt = this.promptTail.then(() => this.runPrompt(message, freshRetryBudget()));
|
|
409
461
|
this.promptTail = queuedPrompt.then(
|
|
410
462
|
() => void 0,
|
|
@@ -421,8 +473,14 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
421
473
|
async runPrompt(message, _budget) {
|
|
422
474
|
if (!this.sdk) await this.start();
|
|
423
475
|
this.authSelfHealUsed = _budget.auth;
|
|
476
|
+
this.limitFailoverUsed = _budget.limit;
|
|
477
|
+
this.limitRejectionSeen = false;
|
|
478
|
+
this.limitTurnRejectedForFailover = false;
|
|
479
|
+
this.providerResponseReported = false;
|
|
480
|
+
this.lastLimitInfo = null;
|
|
481
|
+
this.deferredTurnError = null;
|
|
424
482
|
let retrying = false;
|
|
425
|
-
const isFirstAttempt = !_budget.staleResume && !_budget.poison && !_budget.auth && !_budget.queryEnded;
|
|
483
|
+
const isFirstAttempt = !_budget.staleResume && !_budget.poison && !_budget.auth && !_budget.queryEnded && !_budget.limit;
|
|
426
484
|
if (isFirstAttempt) {
|
|
427
485
|
this._lastTokens = null;
|
|
428
486
|
this._lastContextTokens = null;
|
|
@@ -475,12 +533,26 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
475
533
|
return this.runPrompt(message, { ..._budget, poison: true });
|
|
476
534
|
}
|
|
477
535
|
}
|
|
478
|
-
const
|
|
479
|
-
if (
|
|
536
|
+
const authOwns = err instanceof AuthError && !_budget.auth && this.config.onAuthError != null;
|
|
537
|
+
if (authOwns) {
|
|
480
538
|
if (await this.attemptAuthSelfHeal(err)) {
|
|
481
539
|
retrying = true;
|
|
482
540
|
return this.runPrompt(message, { ..._budget, auth: true });
|
|
483
541
|
}
|
|
542
|
+
} else if (!_budget.limit && this.limitRejectionSeen && this.config.onLimitBlocked) {
|
|
543
|
+
if (await this.attemptLimitFailover(err)) {
|
|
544
|
+
if (this.abortController?.signal.aborted) {
|
|
545
|
+
this.log.info("aborted during the seat failover; not replaying the prompt");
|
|
546
|
+
this.emitAgentEnd();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
retrying = true;
|
|
550
|
+
return this.runPrompt(message, { ..._budget, limit: true });
|
|
551
|
+
}
|
|
552
|
+
if (this.limitTurnRejectedForFailover) {
|
|
553
|
+
this.emitAgentEnd();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
484
556
|
}
|
|
485
557
|
throw err;
|
|
486
558
|
} finally {
|
|
@@ -489,6 +561,10 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
489
561
|
this.turnResolve = null;
|
|
490
562
|
this.turnReject = null;
|
|
491
563
|
this.running = false;
|
|
564
|
+
this.limitRejectionSeen = false;
|
|
565
|
+
this.limitTurnRejectedForFailover = false;
|
|
566
|
+
this.lastLimitInfo = null;
|
|
567
|
+
this.deferredTurnError = null;
|
|
492
568
|
}
|
|
493
569
|
}
|
|
494
570
|
}
|
|
@@ -802,18 +878,60 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
802
878
|
this.running = false;
|
|
803
879
|
return true;
|
|
804
880
|
}
|
|
881
|
+
this.emitDeferredTurnError({ type: "error", error: err.message, detail: err.detail });
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Side-effects of the seat-failover path: ask the host to re-resolve the AI
|
|
886
|
+
* provider config behind this session. On `switched: true` drop the query and
|
|
887
|
+
* return `true` (the caller recurses onto a fresh CLI); otherwise emit the
|
|
888
|
+
* deferred error and return `false`. Mirrors {@link attemptAuthSelfHeal} —
|
|
889
|
+
* this performs the state mutation, the caller owns the control flow.
|
|
890
|
+
*
|
|
891
|
+
* Dropping `this.query` is the whole point of the retry, not bookkeeping: a
|
|
892
|
+
* resident CLI caches "blocked until <reset>" and short-circuits the next
|
|
893
|
+
* prompt locally with no API attempt at all (skaile-ai/workspaces#566), so a
|
|
894
|
+
* rewritten credentials file alone would change nothing. Only a fresh spawn
|
|
895
|
+
* clears it. `sessionId` / `hasSession` are preserved so the new query
|
|
896
|
+
* resumes the same conversation.
|
|
897
|
+
*/
|
|
898
|
+
async attemptLimitFailover(err) {
|
|
899
|
+
this.log.warn("turn blocked by an upstream usage limit; invoking onLimitBlocked", {
|
|
900
|
+
configId: this.config.aiProviderConfigId,
|
|
901
|
+
limitType: this.lastLimitInfo?.limitType,
|
|
902
|
+
resetsAt: this.lastLimitInfo?.resetsAt
|
|
903
|
+
});
|
|
904
|
+
let outcome;
|
|
805
905
|
try {
|
|
806
|
-
this.
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
detail: err.detail
|
|
906
|
+
outcome = await this.config.onLimitBlocked({
|
|
907
|
+
configId: this.config.aiProviderConfigId ?? "",
|
|
908
|
+
...this.lastLimitInfo ?? {}
|
|
810
909
|
});
|
|
811
|
-
} catch (
|
|
812
|
-
this.log.warn(
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
910
|
+
} catch (failoverErr) {
|
|
911
|
+
this.log.warn("onLimitBlocked callback threw; surfacing the original limit error", {
|
|
912
|
+
error: failoverErr instanceof Error ? failoverErr.message : String(failoverErr)
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
if (outcome?.switched === true) {
|
|
916
|
+
this.log.info("switched AI provider config; respawning the CLI and replaying the prompt", {
|
|
917
|
+
configId: outcome.configId
|
|
918
|
+
});
|
|
919
|
+
this.query = null;
|
|
920
|
+
this.endInputStream();
|
|
921
|
+
this.turnResolve = null;
|
|
922
|
+
this.turnReject = null;
|
|
923
|
+
this.running = false;
|
|
924
|
+
return true;
|
|
816
925
|
}
|
|
926
|
+
this.log.info("no alternative AI provider config available; surfacing the limit error", {
|
|
927
|
+
reason: outcome?.reason
|
|
928
|
+
});
|
|
929
|
+
const detail = err?.detail;
|
|
930
|
+
this.emitDeferredTurnError({
|
|
931
|
+
type: "error",
|
|
932
|
+
error: err instanceof Error ? err.message : String(err),
|
|
933
|
+
...detail ? { detail } : {}
|
|
934
|
+
});
|
|
817
935
|
return false;
|
|
818
936
|
}
|
|
819
937
|
/**
|
|
@@ -1260,6 +1378,12 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1260
1378
|
category: detail.category,
|
|
1261
1379
|
retryable: detail.retryable
|
|
1262
1380
|
});
|
|
1381
|
+
if (detail.category === "auth") {
|
|
1382
|
+
this.reportProviderResponse({ status: 401 });
|
|
1383
|
+
} else if (isSeatCapacityFailure(detail)) {
|
|
1384
|
+
this.limitRejectionSeen = true;
|
|
1385
|
+
this.reportProviderResponse({ status: 429, ...this.lastLimitInfo ?? {} });
|
|
1386
|
+
}
|
|
1263
1387
|
const surfaced = detail.category === "auth" ? new AuthError(detail) : detail.category === "account" ? new AccountUnavailableError(detail) : error;
|
|
1264
1388
|
this.failTurn(surfaced, detail);
|
|
1265
1389
|
}
|
|
@@ -1280,10 +1404,14 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1280
1404
|
this.turnResolve = null;
|
|
1281
1405
|
this.turnReject = null;
|
|
1282
1406
|
}
|
|
1407
|
+
this.emitAgentEnd();
|
|
1408
|
+
}
|
|
1409
|
+
/** Emit the turn-end signal, swallowing a throwing listener. */
|
|
1410
|
+
emitAgentEnd() {
|
|
1283
1411
|
try {
|
|
1284
1412
|
this.emit("agent-event", { type: "agent_end" });
|
|
1285
1413
|
} catch (emitErr) {
|
|
1286
|
-
this.log.warn("agent-event listener threw
|
|
1414
|
+
this.log.warn("agent-event listener threw while ending the turn; suppressing", {
|
|
1287
1415
|
error: emitErr instanceof Error ? emitErr.message : String(emitErr)
|
|
1288
1416
|
});
|
|
1289
1417
|
}
|
|
@@ -1343,29 +1471,62 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1343
1471
|
*/
|
|
1344
1472
|
failTurn(err, detail) {
|
|
1345
1473
|
this.disarmTurnStallWatchdog();
|
|
1474
|
+
const hadPendingTurn = this.turnReject != null;
|
|
1346
1475
|
if (this.turnReject) {
|
|
1347
1476
|
this.turnReject(err);
|
|
1348
1477
|
this.turnResolve = null;
|
|
1349
1478
|
this.turnReject = null;
|
|
1350
1479
|
}
|
|
1351
1480
|
this.turnCompleted = true;
|
|
1481
|
+
const event = {
|
|
1482
|
+
type: "error",
|
|
1483
|
+
error: err.message,
|
|
1484
|
+
...detail ? { detail } : {}
|
|
1485
|
+
};
|
|
1352
1486
|
const willSelfHeal = err instanceof AuthError && !this.authSelfHealUsed && this.config.onAuthError != null;
|
|
1353
|
-
if (willSelfHeal) {
|
|
1487
|
+
if (hadPendingTurn && willSelfHeal) {
|
|
1354
1488
|
this.log.info("deferring auth-error event; self-heal will attempt refresh");
|
|
1489
|
+
this.deferredTurnError = event;
|
|
1355
1490
|
return;
|
|
1356
1491
|
}
|
|
1492
|
+
if (hadPendingTurn && this.willFailoverOnLimit()) {
|
|
1493
|
+
this.log.info("deferring limit error event; seat failover will be attempted");
|
|
1494
|
+
this.deferredTurnError = event;
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
this.emitTurnErrorEvent(event);
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Whether a usage-limit block on the current attempt still has a seat
|
|
1501
|
+
* failover available. Evaluated identically by {@link failTurn} (to decide
|
|
1502
|
+
* whether to withhold the error event) and by {@link handleResultErrorPayload}
|
|
1503
|
+
* (to decide whether to reject the turn at all) — one predicate so the two
|
|
1504
|
+
* can never disagree and strand a deferred event.
|
|
1505
|
+
*/
|
|
1506
|
+
willFailoverOnLimit() {
|
|
1507
|
+
return this.limitRejectionSeen && !this.limitFailoverUsed && this.config.onLimitBlocked != null;
|
|
1508
|
+
}
|
|
1509
|
+
/** Emit one `agent-event: error`, swallowing a throwing listener. */
|
|
1510
|
+
emitTurnErrorEvent(event) {
|
|
1357
1511
|
try {
|
|
1358
|
-
this.emit("agent-event",
|
|
1359
|
-
type: "error",
|
|
1360
|
-
error: err.message,
|
|
1361
|
-
...detail ? { detail } : {}
|
|
1362
|
-
});
|
|
1512
|
+
this.emit("agent-event", event);
|
|
1363
1513
|
} catch (emitErr) {
|
|
1364
|
-
this.log.warn("agent-event listener threw
|
|
1514
|
+
this.log.warn("agent-event listener threw while surfacing a turn error; suppressing", {
|
|
1365
1515
|
error: emitErr instanceof Error ? emitErr.message : String(emitErr)
|
|
1366
1516
|
});
|
|
1367
1517
|
}
|
|
1368
1518
|
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Emit the error {@link failTurn} withheld, exactly once. Called by a
|
|
1521
|
+
* recovery path that decided NOT to retry, so every failure produces one
|
|
1522
|
+
* downstream error event and a recovered one produces none. `fallback` covers
|
|
1523
|
+
* the paths that reject without going through `failTurn`.
|
|
1524
|
+
*/
|
|
1525
|
+
emitDeferredTurnError(fallback) {
|
|
1526
|
+
const event = this.deferredTurnError ?? fallback;
|
|
1527
|
+
this.deferredTurnError = null;
|
|
1528
|
+
this.emitTurnErrorEvent(event);
|
|
1529
|
+
}
|
|
1369
1530
|
// ---------------------------------------------------------------------------
|
|
1370
1531
|
// Message handling
|
|
1371
1532
|
// ---------------------------------------------------------------------------
|
|
@@ -1382,9 +1543,17 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1382
1543
|
}
|
|
1383
1544
|
}
|
|
1384
1545
|
/**
|
|
1385
|
-
* Surface subscription rate-limit telemetry as a structured warning log
|
|
1546
|
+
* Surface subscription rate-limit telemetry as a structured warning log, and
|
|
1547
|
+
* — on `status: "rejected"` — arm the seat failover and report the rejection
|
|
1548
|
+
* to the host for per-seat health attribution.
|
|
1549
|
+
*
|
|
1386
1550
|
* Returns `true` when the message was a rate-limit event (and fully handled),
|
|
1387
1551
|
* so the caller can stop dispatching.
|
|
1552
|
+
*
|
|
1553
|
+
* This used to only log. That is what left a session pinned to a parked seat:
|
|
1554
|
+
* the rejection was fully handled here and nothing propagated, so the
|
|
1555
|
+
* platform's re-resolution — which was ready and working — was reachable only
|
|
1556
|
+
* through a token refresh that happened to be due (skaile-ai/platform#2976).
|
|
1388
1557
|
*/
|
|
1389
1558
|
handleRateLimitEvent(msg) {
|
|
1390
1559
|
const maybeRateLimit = msg;
|
|
@@ -1396,10 +1565,39 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1396
1565
|
utilization: info.utilization,
|
|
1397
1566
|
resetsAt: info.resetsAt
|
|
1398
1567
|
});
|
|
1568
|
+
if (info.status === "rejected") {
|
|
1569
|
+
this.limitRejectionSeen = true;
|
|
1570
|
+
this.lastLimitInfo = {
|
|
1571
|
+
...info.rateLimitType ? { limitType: info.rateLimitType } : {},
|
|
1572
|
+
...typeof info.resetsAt === "number" ? { resetsAt: info.resetsAt } : {},
|
|
1573
|
+
...typeof info.utilization === "number" ? { utilization: info.utilization } : {}
|
|
1574
|
+
};
|
|
1575
|
+
this.reportProviderResponse({ status: 429, ...this.lastLimitInfo });
|
|
1576
|
+
}
|
|
1399
1577
|
return true;
|
|
1400
1578
|
}
|
|
1401
1579
|
return false;
|
|
1402
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Report an observed 401 / 429 to the host so the owning AI provider config's
|
|
1583
|
+
* live health counters can be bumped (skaile-ai/workspaces#368). Strictly
|
|
1584
|
+
* fire-and-forget: a throwing host callback must never fail the turn that
|
|
1585
|
+
* produced the observation.
|
|
1586
|
+
*/
|
|
1587
|
+
reportProviderResponse(observation) {
|
|
1588
|
+
if (!this.config.onProviderResponse || this.providerResponseReported) return;
|
|
1589
|
+
this.providerResponseReported = true;
|
|
1590
|
+
try {
|
|
1591
|
+
this.config.onProviderResponse({
|
|
1592
|
+
...observation,
|
|
1593
|
+
...this.config.aiProviderConfigId ? { configId: this.config.aiProviderConfigId } : {}
|
|
1594
|
+
});
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
this.log.warn("onProviderResponse threw; suppressing", {
|
|
1597
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1403
1601
|
handleAssistantMessage(msg) {
|
|
1404
1602
|
this.captureAssistantContext(msg);
|
|
1405
1603
|
const agentMsg = this.mapAssistantMessage(msg);
|
|
@@ -1593,11 +1791,20 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1593
1791
|
category: detail.category,
|
|
1594
1792
|
retryable: detail.retryable
|
|
1595
1793
|
});
|
|
1794
|
+
if (detail.category === "auth") {
|
|
1795
|
+
this.reportProviderResponse({ status: 401 });
|
|
1796
|
+
} else if (isSeatCapacityFailure(detail, msg.subtype)) {
|
|
1797
|
+
this.limitRejectionSeen = true;
|
|
1798
|
+
this.reportProviderResponse({ status: 429, ...this.lastLimitInfo ?? {} });
|
|
1799
|
+
}
|
|
1596
1800
|
const errorText = errors.join("; ") || `Agent stopped: ${msg.subtype}`;
|
|
1597
1801
|
if (detail.category === "auth") {
|
|
1598
1802
|
this.failTurn(new AuthError({ ...detail, message: errorText }), detail);
|
|
1599
1803
|
} else if (detail.category === "account") {
|
|
1600
1804
|
this.failTurn(new AccountUnavailableError({ ...detail, message: errorText }), detail);
|
|
1805
|
+
} else if (this.willFailoverOnLimit()) {
|
|
1806
|
+
this.limitTurnRejectedForFailover = true;
|
|
1807
|
+
this.failTurn(new Error(errorText), detail);
|
|
1601
1808
|
} else if (errors.length > 0) {
|
|
1602
1809
|
this.emit("agent-event", {
|
|
1603
1810
|
type: "error",
|