@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.
Files changed (148) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/dist/app-sdk/index.js +1 -1
  3. package/dist/{asset-feeds-44M2GV2W.js → asset-feeds-7HM3HONM.js} +4 -4
  4. package/dist/{asset-feeds-44M2GV2W.js.map → asset-feeds-7HM3HONM.js.map} +1 -1
  5. package/dist/asset-manager/index.js +1 -1
  6. package/dist/bridge/drivers/claude-sdk.js +231 -24
  7. package/dist/bridge/drivers/claude-sdk.js.map +1 -1
  8. package/dist/bridge/drivers/codex.js +1 -1
  9. package/dist/bridge/drivers/continue.js +2 -2
  10. package/dist/bridge/drivers/echo.js +1 -1
  11. package/dist/bridge/drivers/gemini.js +3 -3
  12. package/dist/bridge/drivers/goose.js +2 -2
  13. package/dist/bridge/drivers/omp.js +1 -1
  14. package/dist/bridge/drivers/opencode.js +2 -2
  15. package/dist/bridge/drivers/qwen.js +3 -3
  16. package/dist/bridge/index.js +2 -2
  17. package/dist/bridge/src/drivers/claude-sdk.d.ts +96 -1
  18. package/dist/bridge/src/drivers/claude-sdk.d.ts.map +1 -1
  19. package/dist/bridge/src/types.d.ts +103 -0
  20. package/dist/bridge/src/types.d.ts.map +1 -1
  21. package/dist/{chunk-IKNQW3AW.js → chunk-2GREL3OD.js} +3 -3
  22. package/dist/{chunk-IKNQW3AW.js.map → chunk-2GREL3OD.js.map} +1 -1
  23. package/dist/{chunk-Z7ILXHIT.js → chunk-2JW5SWIU.js} +6 -6
  24. package/dist/{chunk-Z7ILXHIT.js.map → chunk-2JW5SWIU.js.map} +1 -1
  25. package/dist/{chunk-J4I2YPGE.js → chunk-4WTC6BSV.js} +3 -3
  26. package/dist/{chunk-J4I2YPGE.js.map → chunk-4WTC6BSV.js.map} +1 -1
  27. package/dist/{chunk-JTBF4OB2.js → chunk-6GDVGAIZ.js} +587 -114
  28. package/dist/chunk-6GDVGAIZ.js.map +1 -0
  29. package/dist/{chunk-SID35UAK.js → chunk-7SHXLJQP.js} +3 -3
  30. package/dist/{chunk-SID35UAK.js.map → chunk-7SHXLJQP.js.map} +1 -1
  31. package/dist/{chunk-JF73DP5W.js → chunk-B7TA2XI7.js} +2 -2
  32. package/dist/{chunk-JF73DP5W.js.map → chunk-B7TA2XI7.js.map} +1 -1
  33. package/dist/{chunk-32TJ55W5.js → chunk-BGAAZMBK.js} +2 -2
  34. package/dist/chunk-BGAAZMBK.js.map +1 -0
  35. package/dist/{chunk-XCG63ZNJ.js → chunk-DGDELPOD.js} +3 -3
  36. package/dist/{chunk-XCG63ZNJ.js.map → chunk-DGDELPOD.js.map} +1 -1
  37. package/dist/{chunk-26L2B6P6.js → chunk-FUCYTGAE.js} +4 -4
  38. package/dist/chunk-FUCYTGAE.js.map +1 -0
  39. package/dist/{chunk-F7S6NDIW.js → chunk-IH2KJYAU.js} +142 -13
  40. package/dist/chunk-IH2KJYAU.js.map +1 -0
  41. package/dist/{chunk-VXITGB7G.js → chunk-KNPBMR2P.js} +3 -3
  42. package/dist/{chunk-VXITGB7G.js.map → chunk-KNPBMR2P.js.map} +1 -1
  43. package/dist/{chunk-BCXI27BH.js → chunk-NVYEK4FI.js} +3 -3
  44. package/dist/{chunk-BCXI27BH.js.map → chunk-NVYEK4FI.js.map} +1 -1
  45. package/dist/{chunk-D4VHBAJC.js → chunk-O3LEV6KB.js} +4 -4
  46. package/dist/{chunk-D4VHBAJC.js.map → chunk-O3LEV6KB.js.map} +1 -1
  47. package/dist/{chunk-2GA2UGQI.js → chunk-OZZCPTOG.js} +6 -6
  48. package/dist/{chunk-2GA2UGQI.js.map → chunk-OZZCPTOG.js.map} +1 -1
  49. package/dist/{chunk-NXHRSUKL.js → chunk-PRUCNSBI.js} +4 -4
  50. package/dist/{chunk-NXHRSUKL.js.map → chunk-PRUCNSBI.js.map} +1 -1
  51. package/dist/{chunk-J7SEUNPD.js → chunk-QVQH6GV2.js} +2 -2
  52. package/dist/{chunk-J7SEUNPD.js.map → chunk-QVQH6GV2.js.map} +1 -1
  53. package/dist/{chunk-I5TSXDTD.js → chunk-RVXBOKHK.js} +3 -3
  54. package/dist/{chunk-I5TSXDTD.js.map → chunk-RVXBOKHK.js.map} +1 -1
  55. package/dist/cli/index.js +30 -30
  56. package/dist/connectors/index.js +3 -3
  57. package/dist/connectors/src/connector-manager.d.ts +34 -2
  58. package/dist/connectors/src/connector-manager.d.ts.map +1 -1
  59. package/dist/connectors/src/connector-prompt.d.ts.map +1 -1
  60. package/dist/connectors/src/connector-types.d.ts +25 -0
  61. package/dist/connectors/src/connector-types.d.ts.map +1 -1
  62. package/dist/connectors/src/credential-helper-script.d.ts +48 -0
  63. package/dist/connectors/src/credential-helper-script.d.ts.map +1 -1
  64. package/dist/connectors/src/index.d.ts +3 -3
  65. package/dist/connectors/src/index.d.ts.map +1 -1
  66. package/dist/connectors/src/managed-gitconfig.d.ts +80 -0
  67. package/dist/connectors/src/managed-gitconfig.d.ts.map +1 -1
  68. package/dist/{connectors-DY7UBBV5.js → connectors-2OHRZDJZ.js} +3 -3
  69. package/dist/{connectors-DY7UBBV5.js.map → connectors-2OHRZDJZ.js.map} +1 -1
  70. package/dist/{ensure-sources-QRURV6JB.js → ensure-sources-42LTJ5QI.js} +4 -4
  71. package/dist/{ensure-sources-QRURV6JB.js.map → ensure-sources-42LTJ5QI.js.map} +1 -1
  72. package/dist/factory-assets/connectors/box.js +3 -3
  73. package/dist/factory-assets/connectors/deploy.js +3 -3
  74. package/dist/factory-assets/connectors/devserver.js +3 -3
  75. package/dist/factory-assets/connectors/flow/adapter.js +3 -3
  76. package/dist/factory-assets/connectors/flow/run-flow.js +4 -4
  77. package/dist/factory-assets/connectors/flow.js +3 -3
  78. package/dist/factory-assets/connectors/git/driver.d.ts +64 -1
  79. package/dist/factory-assets/connectors/git/driver.d.ts.map +1 -1
  80. package/dist/factory-assets/connectors/git.js +3 -3
  81. package/dist/factory-assets/connectors/gmail.js +3 -3
  82. package/dist/factory-assets/connectors/googledrive.js +3 -3
  83. package/dist/factory-assets/connectors/local.js +3 -3
  84. package/dist/factory-assets/connectors/mattermost.js +3 -3
  85. package/dist/factory-assets/connectors/memory.js +3 -3
  86. package/dist/factory-assets/connectors/minio.js +3 -3
  87. package/dist/factory-assets/connectors/postgres.js +3 -3
  88. package/dist/factory-assets/connectors/s3.js +3 -3
  89. package/dist/factory-assets/connectors/sharepoint.js +3 -3
  90. package/dist/factory-assets/connectors/sqlite.js +3 -3
  91. package/dist/factory-assets/connectors/static-server.js +3 -3
  92. package/dist/factory-assets/connectors/tunnel.js +3 -3
  93. package/dist/factory-assets/connectors/webdav.js +3 -3
  94. package/dist/factory-assets/connectors/xstate-store.js +3 -3
  95. package/dist/factory-assets/connectors/xstate.js +3 -3
  96. package/dist/{helpers-TLTVVZRR.js → helpers-BP7FX6CO.js} +4 -4
  97. package/dist/{helpers-TLTVVZRR.js.map → helpers-BP7FX6CO.js.map} +1 -1
  98. package/dist/{library-publish-6HXFLOXC.js → library-publish-X5Z32WJY.js} +6 -6
  99. package/dist/{library-publish-6HXFLOXC.js.map → library-publish-X5Z32WJY.js.map} +1 -1
  100. package/dist/{open-library-FMVJICL5.js → open-library-PHUMBYHP.js} +5 -5
  101. package/dist/{open-library-FMVJICL5.js.map → open-library-PHUMBYHP.js.map} +1 -1
  102. package/dist/{project-BG3N4I3V.js → project-HAMCXPNE.js} +5 -5
  103. package/dist/{project-BG3N4I3V.js.map → project-HAMCXPNE.js.map} +1 -1
  104. package/dist/runner/index.js +7 -7
  105. package/dist/runner/src/ai-provider-switch.d.ts +95 -0
  106. package/dist/runner/src/ai-provider-switch.d.ts.map +1 -0
  107. package/dist/runner/src/credential-rotated.d.ts +23 -4
  108. package/dist/runner/src/credential-rotated.d.ts.map +1 -1
  109. package/dist/runner/src/serve.d.ts.map +1 -1
  110. package/dist/runner/src/session-builder.d.ts +19 -0
  111. package/dist/runner/src/session-builder.d.ts.map +1 -1
  112. package/dist/sdk/asset-manager.js +1 -1
  113. package/dist/sdk/bridge.js +2 -2
  114. package/dist/sdk/index.js +7 -7
  115. package/dist/sdk/runner.js +7 -7
  116. package/dist/sdk/session.js +2 -2
  117. package/dist/sdk/types.js +1 -1
  118. package/dist/session/index.js +2 -2
  119. package/dist/{setup-O2N6PA7I.js → setup-IOOGR3EL.js} +4 -4
  120. package/dist/{setup-O2N6PA7I.js.map → setup-IOOGR3EL.js.map} +1 -1
  121. package/dist/{source-JDIBJZ3X.js → source-772GLNM6.js} +6 -6
  122. package/dist/{source-JDIBJZ3X.js.map → source-772GLNM6.js.map} +1 -1
  123. package/dist/{source-manifest-publish-567T25M6.js → source-manifest-publish-LAUGDQ5G.js} +7 -7
  124. package/dist/{source-manifest-publish-567T25M6.js.map → source-manifest-publish-LAUGDQ5G.js.map} +1 -1
  125. package/dist/tui/index.js +7 -7
  126. package/dist/types/index.js +1 -1
  127. package/dist/types/src/events.d.ts +34 -1
  128. package/dist/types/src/events.d.ts.map +1 -1
  129. package/dist/types/src/index.d.ts +1 -1
  130. package/dist/types/src/index.d.ts.map +1 -1
  131. package/dist/types/src/lifecycle.d.ts +13 -0
  132. package/dist/types/src/lifecycle.d.ts.map +1 -1
  133. package/dist/types/src/version.d.ts +22 -1
  134. package/dist/types/src/version.d.ts.map +1 -1
  135. package/dist/workspace-plugin/adapters/mcp.js +2 -2
  136. package/dist/workspace-plugin/adapters/mcp.js.map +1 -1
  137. package/dist/workspace-plugin/adapters/omp.js +8 -8
  138. package/dist/workspace-plugin/adapters/omp.js.map +1 -1
  139. package/dist/workspace-plugin/index.js +1 -1
  140. package/dist/workspace-plugin/src/adapters/mcp.d.ts.map +1 -1
  141. package/dist/workspace-plugin/src/adapters/omp.d.ts.map +1 -1
  142. package/dist/workspace-plugin/src/tools/connectors.d.ts +7 -3
  143. package/dist/workspace-plugin/src/tools/connectors.d.ts.map +1 -1
  144. package/package.json +1 -1
  145. package/dist/chunk-26L2B6P6.js.map +0 -1
  146. package/dist/chunk-32TJ55W5.js.map +0 -1
  147. package/dist/chunk-F7S6NDIW.js.map +0 -1
  148. 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-32TJ55W5.js';
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("superseded by a new prompt");
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 isAuthError = err instanceof AuthError && !_budget.auth;
479
- if (isAuthError && this.config.onAuthError) {
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.emit("agent-event", {
807
- type: "error",
808
- error: err.message,
809
- detail: err.detail
906
+ outcome = await this.config.onLimitBlocked({
907
+ configId: this.config.aiProviderConfigId ?? "",
908
+ ...this.lastLimitInfo ?? {}
810
909
  });
811
- } catch (emitErr) {
812
- this.log.warn(
813
- "agent-event listener threw during deferred auth-error surfacing; suppressing",
814
- { error: emitErr instanceof Error ? emitErr.message : String(emitErr) }
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 during completeTurn; suppressing", {
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 during failTurn; suppressing", {
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",