shortcutxl 0.3.70 → 0.3.71

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.71]
4
+
5
+ - **Responsive retry recovery** - CLI sessions now keep showing retry progress and can cancel an active retry without freezing.
6
+
3
7
  ## [0.3.70]
4
8
 
5
9
  - **GPT-5.6 Sol default** - ShortcutXL now defaults CLI and embedded sessions to GPT-5.6 Sol and shows clearer compact model labels.
@@ -499,7 +499,7 @@ export declare class AgentSession {
499
499
  warnings: string[];
500
500
  }>;
501
501
  /** Cancel in-progress recovery. */
502
- abortRetry(): void;
502
+ abortRetry(): Promise<void>;
503
503
  /** Whether error recovery is currently in progress */
504
504
  get isRetrying(): boolean;
505
505
  /** Whether auto-retry is enabled */
@@ -452,16 +452,8 @@ export class AgentSession {
452
452
  await this._extensions.extensionRunner.runPostTurnHandlers(turnIndex, event.message, event.toolResults);
453
453
  }
454
454
  // --- Step 5: Retry success tracking (sync) ---
455
- // Resolve the retry promise only when the retried agent loop finishes
456
- // successfully (agent_end with no error). Resolving on message_end was a
457
- // race condition: session.prompt() would return while the retry loop was
458
- // still executing tool calls, causing print-mode to exit and kill the
459
- // in-flight tool execution.
460
- if (event.type === 'agent_end' && this._recovery.attempt > 0) {
461
- const lastAssistant = this._findLastAssistantInMessages(event.messages);
462
- if (!lastAssistant || lastAssistant.stopReason !== 'error') {
463
- this._recovery.onSuccess();
464
- }
455
+ if (event.type === 'agent_end') {
456
+ this._recovery.settleAgentEnd(this._findLastAssistantInMessages(event.messages), this._checkContextOverflow);
465
457
  }
466
458
  // --- Step 6: Post-agent: stop hooks → retry → compaction (async, sequential, conditional) ---
467
459
  if (event.type === 'agent_end') {
@@ -1016,6 +1008,8 @@ export class AgentSession {
1016
1008
  await this.agent.prompt(messages);
1017
1009
  await this._agentEventQueue.catch(() => { });
1018
1010
  await this._recovery.wait();
1011
+ // Keep prompt open until a cancelled continuation observes its abort signal.
1012
+ await this.agent.waitForIdle();
1019
1013
  }
1020
1014
  finally {
1021
1015
  await this._agentEventQueue.catch(() => { });
@@ -1051,6 +1045,7 @@ export class AgentSession {
1051
1045
  await this.agent.prompt([buildGoalContinuationMessage(goal)]);
1052
1046
  await this._agentEventQueue.catch(() => { });
1053
1047
  await this._recovery.wait();
1048
+ await this.agent.waitForIdle();
1054
1049
  }
1055
1050
  finally {
1056
1051
  await this._agentEventQueue.catch(() => { });
@@ -1434,8 +1429,13 @@ export class AgentSession {
1434
1429
  // Error Recovery (delegated to SessionErrorRecovery)
1435
1430
  // =========================================================================
1436
1431
  /** Cancel in-progress recovery. */
1437
- abortRetry() {
1432
+ async abortRetry() {
1433
+ const wasRetrying = this._recovery.isRetrying;
1438
1434
  this._recovery.abort();
1435
+ if (!wasRetrying)
1436
+ return;
1437
+ this.agent.abort();
1438
+ await this.agent.waitForIdle();
1439
1439
  }
1440
1440
  /** Whether error recovery is currently in progress */
1441
1441
  get isRetrying() {
@@ -17,6 +17,7 @@ export function resolveShortcutApiV2Transport(modelId) {
17
17
  const normalized = modelId.trim().toLowerCase();
18
18
  // Shortcut-hosted "Pivot" and Fireworks GLM both speak OpenAI completions.
19
19
  if (normalized === SHORTCUT_MODEL_ID.Hosted ||
20
+ normalized === SHORTCUT_MODEL_ID.Grok45 ||
20
21
  normalized === SHORTCUT_MODEL_ID.Glm52Fireworks.toLowerCase() ||
21
22
  normalized.startsWith('accounts/fireworks/')) {
22
23
  return 'openai-completions';
package/dist/cli.js CHANGED
@@ -283150,6 +283150,7 @@ var init_model_ids = __esm({
283150
283150
  ClaudeOpus48: "claude-opus-4-8",
283151
283151
  Glm52Fireworks: "accounts/fireworks/models/glm-5p2",
283152
283152
  Gpt56Sol: "gpt-5.6-sol",
283153
+ Grok45: "grok-4.5",
283153
283154
  Hosted: "pivot",
283154
283155
  Gemini3Flash: "gemini-3-flash-preview",
283155
283156
  ClaudeHaiku45: "claude-haiku-4-5-20251001"
@@ -283159,6 +283160,7 @@ var init_model_ids = __esm({
283159
283160
  ClaudeOpus48: `${SHORTCUT_PROVIDER_ID}/${SHORTCUT_MODEL_ID.ClaudeOpus48}`,
283160
283161
  Glm52Fireworks: `${SHORTCUT_PROVIDER_ID}/${SHORTCUT_MODEL_ID.Glm52Fireworks}`,
283161
283162
  Gpt56Sol: `${SHORTCUT_PROVIDER_ID}/${SHORTCUT_MODEL_ID.Gpt56Sol}`,
283163
+ Grok45: `${SHORTCUT_PROVIDER_ID}/${SHORTCUT_MODEL_ID.Grok45}`,
283162
283164
  Hosted: `${SHORTCUT_PROVIDER_ID}/${SHORTCUT_MODEL_ID.Hosted}`
283163
283165
  };
283164
283166
  SHORTCUT_OPENAI_MODEL_ID = SHORTCUT_MODEL_ID.Gpt56Sol;
@@ -323554,6 +323556,17 @@ var init_validation = __esm({
323554
323556
  });
323555
323557
 
323556
323558
  // src/contracts/model-stream.ts
323559
+ function isTerminalModelStreamErrorDetails(details) {
323560
+ if (!details) return false;
323561
+ if (details.status === 402) return true;
323562
+ return details.code !== void 0 && TERMINAL_MODEL_STREAM_ERROR_CODE_SET.has(details.code);
323563
+ }
323564
+ function isInsufficientCreditsModelStreamFailure(details, message) {
323565
+ return details?.status === 402 || details?.code === INSUFFICIENT_CREDITS_ERROR_CODE || message === INSUFFICIENT_CREDITS_SAFE_MESSAGE;
323566
+ }
323567
+ function isTerminalModelStreamFailure(details, message) {
323568
+ return isTerminalModelStreamErrorDetails(details) || message === INSUFFICIENT_CREDITS_SAFE_MESSAGE;
323569
+ }
323557
323570
  function isRecord(value) {
323558
323571
  return value != null && typeof value === "object" && !Array.isArray(value);
323559
323572
  }
@@ -323581,8 +323594,8 @@ function isShortcutXLModelStreamErrorDetails(value) {
323581
323594
  return true;
323582
323595
  }
323583
323596
  function safeModelStreamErrorMessage(details) {
323584
- if (details?.code === "insufficient_credits" || details?.status === 402) {
323585
- return "Insufficient credits. Purchase more credits to continue using paid models.";
323597
+ if (isInsufficientCreditsModelStreamFailure(details, void 0)) {
323598
+ return INSUFFICIENT_CREDITS_SAFE_MESSAGE;
323586
323599
  }
323587
323600
  if (details?.code === "context_overflow") {
323588
323601
  return "The conversation exceeded the model context window.";
@@ -323659,7 +323672,7 @@ function extractStructuredHttpErrorPayload(value) {
323659
323672
  }
323660
323673
  return void 0;
323661
323674
  }
323662
- var LLMErrorMetadataHeaders, ShortcutXLModelStreamFetchStage, STREAM_ERROR_PROVIDER_TYPES, MODEL_ACCESS_DENIED_PROVIDER_ERROR_TYPE, MODEL_ACCESS_DENIED_MESSAGE_PATTERN;
323675
+ var LLMErrorMetadataHeaders, ShortcutXLModelStreamFetchStage, INSUFFICIENT_CREDITS_ERROR_CODE, INSUFFICIENT_CREDITS_SAFE_MESSAGE, TERMINAL_MODEL_STREAM_ERROR_CODES, TERMINAL_MODEL_STREAM_ERROR_CODE_SET, STREAM_ERROR_PROVIDER_TYPES, MODEL_ACCESS_DENIED_PROVIDER_ERROR_TYPE, MODEL_ACCESS_DENIED_MESSAGE_PATTERN;
323663
323676
  var init_model_stream = __esm({
323664
323677
  "src/contracts/model-stream.ts"() {
323665
323678
  "use strict";
@@ -323681,6 +323694,21 @@ var init_model_stream = __esm({
323681
323694
  /** Response started but the SSE read failed or idle-timed out mid-stream. */
323682
323695
  ResponseBody: "response_body"
323683
323696
  };
323697
+ INSUFFICIENT_CREDITS_ERROR_CODE = "insufficient_credits";
323698
+ INSUFFICIENT_CREDITS_SAFE_MESSAGE = "Insufficient credits. Purchase more credits to continue using paid models.";
323699
+ TERMINAL_MODEL_STREAM_ERROR_CODES = [
323700
+ INSUFFICIENT_CREDITS_ERROR_CODE,
323701
+ "customer_credential_scope_required",
323702
+ "customer_credential_resolution_failed",
323703
+ "customer_credential_resolution_timeout",
323704
+ "customer_credential_resolution_cancelled",
323705
+ "customer_credential_unconfigured",
323706
+ "unsupported_credential_mode",
323707
+ "model_policy_rejected"
323708
+ ];
323709
+ TERMINAL_MODEL_STREAM_ERROR_CODE_SET = new Set(
323710
+ TERMINAL_MODEL_STREAM_ERROR_CODES
323711
+ );
323684
323712
  STREAM_ERROR_PROVIDER_TYPES = /* @__PURE__ */ new Set([
323685
323713
  "overloaded",
323686
323714
  "rate_limit",
@@ -324045,10 +324073,7 @@ function getErrorDetails(message) {
324045
324073
  }
324046
324074
  function classifyStructuredError(details) {
324047
324075
  if (!details) return null;
324048
- if (details.code !== void 0 && TERMINAL_GATEWAY_ERROR_CODES.has(details.code)) {
324049
- return "terminal";
324050
- }
324051
- if (details.status === 402) return "terminal";
324076
+ if (isTerminalModelStreamErrorDetails(details)) return "terminal";
324052
324077
  if (details.providerErrorType === "unauthorized" || details.status === 401) return "auth_error";
324053
324078
  if (details.providerErrorType === "rate_limit" || details.status === 429) return "rate_limit";
324054
324079
  if (details.providerErrorType === "overloaded" || details.status === 529) return "overloaded";
@@ -324060,22 +324085,28 @@ function classifyStructuredError(details) {
324060
324085
  }
324061
324086
  return null;
324062
324087
  }
324063
- function classifyError(message, isContextOverflow2) {
324064
- if (message.stopReason !== "error") return null;
324065
- if (!message.errorMessage) return null;
324066
- if (isContextOverflow2(message)) return "context_overflow";
324067
- const structuredCategory = classifyStructuredError(getErrorDetails(message));
324088
+ function classifyErrorDisposition(message, isContextOverflow2) {
324089
+ if (message.stopReason !== "error") return { kind: "none" };
324090
+ if (!message.errorMessage) return { kind: "none" };
324091
+ if (isContextOverflow2(message)) return { kind: "categorized", category: "context_overflow" };
324092
+ const details = getErrorDetails(message);
324093
+ const structuredCategory = classifyStructuredError(details);
324068
324094
  if (structuredCategory === "terminal") {
324069
- return null;
324095
+ return { kind: "terminal" };
324070
324096
  }
324071
- if (structuredCategory) return structuredCategory;
324097
+ if (structuredCategory) return { kind: "categorized", category: structuredCategory };
324072
324098
  const err = message.errorMessage;
324073
- if (AUTH_ERROR_PATTERN.test(err)) return "auth_error";
324074
- if (RATE_LIMIT_PATTERN.test(err)) return "rate_limit";
324075
- if (OVERLOADED_PATTERN.test(err)) return "overloaded";
324076
- if (SERVER_ERROR_PATTERN.test(err)) return "server_error";
324077
- if (CONNECTION_ERROR_PATTERN.test(err)) return "server_error";
324078
- return "unknown";
324099
+ if (isTerminalModelStreamFailure(details, err)) return { kind: "terminal" };
324100
+ if (AUTH_ERROR_PATTERN.test(err)) return { kind: "categorized", category: "auth_error" };
324101
+ if (RATE_LIMIT_PATTERN.test(err)) return { kind: "categorized", category: "rate_limit" };
324102
+ if (OVERLOADED_PATTERN.test(err)) return { kind: "categorized", category: "overloaded" };
324103
+ if (SERVER_ERROR_PATTERN.test(err)) return { kind: "categorized", category: "server_error" };
324104
+ if (CONNECTION_ERROR_PATTERN.test(err)) return { kind: "categorized", category: "server_error" };
324105
+ return { kind: "categorized", category: "unknown" };
324106
+ }
324107
+ function classifyError(message, isContextOverflow2) {
324108
+ const disposition = classifyErrorDisposition(message, isContextOverflow2);
324109
+ return disposition.kind === "categorized" ? disposition.category : null;
324079
324110
  }
324080
324111
  function computeDelay(config2, attempt) {
324081
324112
  if (config2.baseDelayMs === 0) return 0;
@@ -324087,26 +324118,17 @@ function computeDelay(config2, attempt) {
324087
324118
  function getCompactionKeepRecentTokens(attempt) {
324088
324119
  return COMPACTION_ESCALATION[Math.min(attempt - 1, COMPACTION_ESCALATION.length - 1)];
324089
324120
  }
324090
- var AUTH_ERROR_PATTERN, RATE_LIMIT_PATTERN, OVERLOADED_PATTERN, SERVER_ERROR_PATTERN, CONNECTION_ERROR_PATTERN, TERMINAL_GATEWAY_ERROR_CODES, SERVER_ERROR_MAX_ATTEMPTS, STRATEGY_CONFIGS, COMPACTION_ESCALATION, SessionErrorRecovery;
324121
+ var AUTH_ERROR_PATTERN, RATE_LIMIT_PATTERN, OVERLOADED_PATTERN, SERVER_ERROR_PATTERN, CONNECTION_ERROR_PATTERN, SERVER_ERROR_MAX_ATTEMPTS, STRATEGY_CONFIGS, COMPACTION_ESCALATION, SessionErrorRecovery;
324091
324122
  var init_session_error_recovery = __esm({
324092
324123
  "src/core/session/session-error-recovery.ts"() {
324093
324124
  "use strict";
324125
+ init_model_stream();
324094
324126
  init_sleep();
324095
324127
  AUTH_ERROR_PATTERN = /\b401\b|invalid.?api.?key|unauthorized|authentication.?failed/i;
324096
324128
  RATE_LIMIT_PATTERN = /rate.?limit|too many requests|\b429\b/i;
324097
324129
  OVERLOADED_PATTERN = /overloaded|\b529\b/i;
324098
324130
  SERVER_ERROR_PATTERN = /\b500\b|\b502\b|\b503\b|\b504\b|\b524\b|service.?unavailable|server error|internal error/i;
324099
324131
  CONNECTION_ERROR_PATTERN = /connection.?error|connection.?refused|other side closed|fetch failed|failed to fetch|upstream.?connect|reset before headers|terminated|retry delay|peer closed|complete message body|load failed|network.?error|internet connection appears to be offline/i;
324100
- TERMINAL_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
324101
- "insufficient_credits",
324102
- "customer_credential_scope_required",
324103
- "customer_credential_resolution_failed",
324104
- "customer_credential_resolution_timeout",
324105
- "customer_credential_resolution_cancelled",
324106
- "customer_credential_unconfigured",
324107
- "unsupported_credential_mode",
324108
- "model_policy_rejected"
324109
- ]);
324110
324132
  SERVER_ERROR_MAX_ATTEMPTS = 5;
324111
324133
  STRATEGY_CONFIGS = {
324112
324134
  rate_limit: {
@@ -324195,11 +324217,13 @@ var init_session_error_recovery = __esm({
324195
324217
  _emit(event) {
324196
324218
  this._listener?.(event);
324197
324219
  }
324198
- async _continueRecovery(category, attempt, actions) {
324199
- try {
324200
- await actions.continue();
324201
- return true;
324202
- } catch (error2) {
324220
+ _continueRecovery(category, attempt, actions) {
324221
+ const activeRecovery = this._promise;
324222
+ void Promise.resolve().then(() => {
324223
+ if (!activeRecovery || this._promise !== activeRecovery) return;
324224
+ return actions.continue();
324225
+ }).catch((error2) => {
324226
+ if (!activeRecovery || this._promise !== activeRecovery) return;
324203
324227
  this._emit({
324204
324228
  type: "recovery_end",
324205
324229
  category,
@@ -324208,17 +324232,37 @@ var init_session_error_recovery = __esm({
324208
324232
  finalError: error2 instanceof Error ? error2.message : String(error2)
324209
324233
  });
324210
324234
  this._reset();
324211
- return false;
324212
- }
324235
+ });
324236
+ return true;
324213
324237
  }
324214
324238
  /** Reset all state to idle. */
324215
324239
  _reset() {
324240
+ this._abortController = void 0;
324216
324241
  this._attempt = 0;
324217
324242
  this._category = null;
324218
324243
  this._resolve?.();
324219
324244
  this._resolve = void 0;
324220
324245
  this._promise = void 0;
324221
324246
  }
324247
+ /** Emit a cancellation event once for an active, visible recovery. */
324248
+ _emitCancelledRecovery() {
324249
+ if (!this._promise || this._attempt <= 0 || !this._category) return;
324250
+ this._emit({
324251
+ type: "recovery_end",
324252
+ category: this._category,
324253
+ success: false,
324254
+ attempt: this._attempt,
324255
+ finalError: "Recovery cancelled"
324256
+ });
324257
+ }
324258
+ /** Cancel recovery state and any in-progress backoff sleep. */
324259
+ _cancelRecovery() {
324260
+ const abortController = this._abortController;
324261
+ this._abortController = void 0;
324262
+ this._emitCancelledRecovery();
324263
+ this._reset();
324264
+ abortController?.abort();
324265
+ }
324222
324266
  /**
324223
324267
  * Create the recovery promise synchronously when an agent_end event arrives.
324224
324268
  *
@@ -324256,6 +324300,15 @@ var init_session_error_recovery = __esm({
324256
324300
  }
324257
324301
  const config2 = STRATEGY_CONFIGS[category];
324258
324302
  if (config2.maxAttempts === 0) {
324303
+ if (this._attempt > 0 || this._promise) {
324304
+ this._emit({
324305
+ type: "recovery_end",
324306
+ category: this._category ?? category,
324307
+ success: false,
324308
+ attempt: this._attempt,
324309
+ finalError: message.errorMessage
324310
+ });
324311
+ }
324259
324312
  this._reset();
324260
324313
  return false;
324261
324314
  }
@@ -324379,6 +324432,7 @@ var init_session_error_recovery = __esm({
324379
324432
  }
324380
324433
  /** Handle abort during sleep. */
324381
324434
  _handleAbort(category) {
324435
+ if (!this._promise) return false;
324382
324436
  const attempt = this._attempt;
324383
324437
  this._abortController = void 0;
324384
324438
  this._emit({
@@ -324406,10 +324460,37 @@ var init_session_error_recovery = __esm({
324406
324460
  });
324407
324461
  this._reset();
324408
324462
  }
324463
+ /**
324464
+ * Called when a continuation turn reaches an unrecoverable assistant error.
324465
+ * This resolves the in-flight recovery as failed instead of treating the
324466
+ * terminal, non-retryable failure as a successful retry.
324467
+ */
324468
+ onTerminalFailure(finalError) {
324469
+ if (this._attempt <= 0) return;
324470
+ const category = this._category ?? "unknown";
324471
+ this._emit({
324472
+ type: "recovery_end",
324473
+ category,
324474
+ success: false,
324475
+ attempt: this._attempt,
324476
+ finalError
324477
+ });
324478
+ this._reset();
324479
+ }
324480
+ /** Settle recovery when a continuation agent loop reaches agent_end. */
324481
+ settleAgentEnd(lastAssistant, isContextOverflow2) {
324482
+ if (this._attempt <= 0) return;
324483
+ if (!lastAssistant || lastAssistant.stopReason !== "error") {
324484
+ this.onSuccess();
324485
+ return;
324486
+ }
324487
+ if (classifyErrorDisposition(lastAssistant, isContextOverflow2).kind === "terminal") {
324488
+ this.onTerminalFailure(lastAssistant.errorMessage);
324489
+ }
324490
+ }
324409
324491
  /** Cancel in-progress recovery. */
324410
324492
  abort() {
324411
- this._abortController?.abort();
324412
- this._reset();
324493
+ this._cancelRecovery();
324413
324494
  }
324414
324495
  /** Wait for any in-progress recovery to complete. */
324415
324496
  async wait() {
@@ -431129,7 +431210,8 @@ function cloneShortcutModelDefinition(model) {
431129
431210
  return {
431130
431211
  ...model,
431131
431212
  input: [...model.input],
431132
- cost: { ...model.cost }
431213
+ cost: { ...model.cost },
431214
+ ...model.compat ? { compat: { ...model.compat } } : {}
431133
431215
  };
431134
431216
  }
431135
431217
  function createShortcutPublicModelDefinitions() {
@@ -431174,6 +431256,21 @@ var init_shortcut_model_catalog = __esm({
431174
431256
  contextWindow: 1e6,
431175
431257
  maxTokens: 128e3
431176
431258
  },
431259
+ {
431260
+ id: SHORTCUT_MODEL_ID.Grok45,
431261
+ name: "Grok 4.5",
431262
+ reasoning: true,
431263
+ input: ["text", "image"],
431264
+ cost: { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
431265
+ contextWindow: 5e5,
431266
+ maxTokens: 128e3,
431267
+ compat: {
431268
+ supportsStore: false,
431269
+ supportsDeveloperRole: false,
431270
+ supportsReasoningEffort: true,
431271
+ maxTokensField: "max_completion_tokens"
431272
+ }
431273
+ },
431177
431274
  {
431178
431275
  id: SHORTCUT_MODEL_ID.Glm52Fireworks,
431179
431276
  name: "GLM 5.2",
@@ -431583,7 +431680,7 @@ var init_google_vertex_gateway = __esm({
431583
431680
  // src/app/providers/shortcut-api-v2-transport.ts
431584
431681
  function resolveShortcutApiV2Transport(modelId) {
431585
431682
  const normalized = modelId.trim().toLowerCase();
431586
- if (normalized === SHORTCUT_MODEL_ID.Hosted || normalized === SHORTCUT_MODEL_ID.Glm52Fireworks.toLowerCase() || normalized.startsWith("accounts/fireworks/")) {
431683
+ if (normalized === SHORTCUT_MODEL_ID.Hosted || normalized === SHORTCUT_MODEL_ID.Grok45 || normalized === SHORTCUT_MODEL_ID.Glm52Fireworks.toLowerCase() || normalized.startsWith("accounts/fireworks/")) {
431587
431684
  return "openai-completions";
431588
431685
  }
431589
431686
  if (normalized.startsWith("claude-")) {
@@ -453414,9 +453511,6 @@ function operationKindToFailureCategory(kind, message) {
453414
453511
  return RunFailureCategory.LlmStreamFailed;
453415
453512
  }
453416
453513
  }
453417
- function isInsufficientCreditsFailure(details) {
453418
- return details?.status === 402 || details?.code === "insufficient_credits";
453419
- }
453420
453514
  function createOperationFailureCause(input) {
453421
453515
  const category = operationKindToFailureCategory(input.opKind, input.errorMessage);
453422
453516
  return {
@@ -453430,6 +453524,17 @@ function createOperationFailureCause(input) {
453430
453524
  };
453431
453525
  }
453432
453526
  function classifyModelStreamFailure(details, message) {
453527
+ if (isInsufficientCreditsModelStreamFailure(details, message)) {
453528
+ return {
453529
+ category: RunFailureCategory.BillingInsufficientCredits,
453530
+ source: RunFailureSource.Billing,
453531
+ httpStatus: details?.status,
453532
+ errorCode: details?.code ?? INSUFFICIENT_CREDITS_ERROR_CODE,
453533
+ retryable: details?.retryable,
453534
+ retryAfterMs: details?.retryAfterMs,
453535
+ transportFailureStage: details?.transportFailureStage
453536
+ };
453537
+ }
453433
453538
  if (details) {
453434
453539
  if (details.providerErrorType === "model_access_denied" || details.code === "model_access_denied") {
453435
453540
  return {
@@ -453472,17 +453577,6 @@ function classifyModelStreamFailure(details, message) {
453472
453577
  transportFailureStage: details.transportFailureStage
453473
453578
  };
453474
453579
  }
453475
- if (isInsufficientCreditsFailure(details)) {
453476
- return {
453477
- category: RunFailureCategory.BillingInsufficientCredits,
453478
- source: RunFailureSource.Billing,
453479
- httpStatus: details.status,
453480
- errorCode: details.code ?? "insufficient_credits",
453481
- retryable: details.retryable,
453482
- retryAfterMs: details.retryAfterMs,
453483
- transportFailureStage: details.transportFailureStage
453484
- };
453485
- }
453486
453580
  if (details.providerErrorType === "network" && details.transportFailureStage === ShortcutXLModelStreamFetchStage.BeforeResponse) {
453487
453581
  return {
453488
453582
  category: RunFailureCategory.LlmFetchFailed,
@@ -453603,7 +453697,7 @@ var init_run_failure = __esm({
453603
453697
  init_error_classification();
453604
453698
  init_session_error_recovery();
453605
453699
  SAFE_MESSAGE_BY_CATEGORY = {
453606
- [RunFailureCategory.BillingInsufficientCredits]: "Insufficient credits. Purchase more credits to continue using paid models.",
453700
+ [RunFailureCategory.BillingInsufficientCredits]: INSUFFICIENT_CREDITS_SAFE_MESSAGE,
453607
453701
  [RunFailureCategory.CompactionFailed]: "Context compaction failed.",
453608
453702
  [RunFailureCategory.CompactionTimeout]: "Context compaction timed out.",
453609
453703
  [RunFailureCategory.ContextTooLong]: "The conversation exceeded the model context window.",
@@ -473984,11 +474078,11 @@ ${budgetText}` : budgetText;
473984
474078
  event.toolResults
473985
474079
  );
473986
474080
  }
473987
- if (event.type === "agent_end" && this._recovery.attempt > 0) {
473988
- const lastAssistant = this._findLastAssistantInMessages(event.messages);
473989
- if (!lastAssistant || lastAssistant.stopReason !== "error") {
473990
- this._recovery.onSuccess();
473991
- }
474081
+ if (event.type === "agent_end") {
474082
+ this._recovery.settleAgentEnd(
474083
+ this._findLastAssistantInMessages(event.messages),
474084
+ this._checkContextOverflow
474085
+ );
473992
474086
  }
473993
474087
  if (event.type === "agent_end") {
473994
474088
  const stopHookContinued = await this._runStopHooks(event.messages);
@@ -474516,6 +474610,7 @@ ${budgetText}` : budgetText;
474516
474610
  await this._agentEventQueue.catch(() => {
474517
474611
  });
474518
474612
  await this._recovery.wait();
474613
+ await this.agent.waitForIdle();
474519
474614
  } finally {
474520
474615
  await this._agentEventQueue.catch(() => {
474521
474616
  });
@@ -474551,6 +474646,7 @@ ${budgetText}` : budgetText;
474551
474646
  await this._agentEventQueue.catch(() => {
474552
474647
  });
474553
474648
  await this._recovery.wait();
474649
+ await this.agent.waitForIdle();
474554
474650
  } finally {
474555
474651
  await this._agentEventQueue.catch(() => {
474556
474652
  });
@@ -474939,8 +475035,12 @@ ${budgetText}` : budgetText;
474939
475035
  // Error Recovery (delegated to SessionErrorRecovery)
474940
475036
  // =========================================================================
474941
475037
  /** Cancel in-progress recovery. */
474942
- abortRetry() {
475038
+ async abortRetry() {
475039
+ const wasRetrying = this._recovery.isRetrying;
474943
475040
  this._recovery.abort();
475041
+ if (!wasRetrying) return;
475042
+ this.agent.abort();
475043
+ await this.agent.waitForIdle();
474944
475044
  }
474945
475045
  /** Whether error recovery is currently in progress */
474946
475046
  get isRetrying() {
@@ -540208,7 +540308,7 @@ ${message}`, ["Yes", "No"], opts);
540208
540308
  handleRecoveryStartEvent(event) {
540209
540309
  this.retryEscapeHandler = this.defaultEditor.onEscape;
540210
540310
  this.defaultEditor.onEscape = () => {
540211
- this.session.abortRetry();
540311
+ void this.session.abortRetry();
540212
540312
  };
540213
540313
  const retryChildren = this.chatContainer.children;
540214
540314
  for (let i2 = retryChildren.length - 1; i2 >= 0; i2--) {
@@ -540240,7 +540340,9 @@ ${message}`, ["Yes", "No"], opts);
540240
540340
  this.retryLoader = void 0;
540241
540341
  this.statusContainer.clear();
540242
540342
  }
540243
- if (!event.success) {
540343
+ if (!event.success && event.finalError === "Recovery cancelled") {
540344
+ this.showStatus("Recovery cancelled");
540345
+ } else if (!event.success) {
540244
540346
  this.showError(
540245
540347
  `Recovery failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`
540246
540348
  );
@@ -544779,7 +544881,7 @@ async function runRpcMode(session) {
544779
544881
  return success2(id, "set_auto_retry");
544780
544882
  }
544781
544883
  case "abort_retry": {
544782
- session.abortRetry();
544884
+ await session.abortRetry();
544783
544885
  return success2(id, "abort_retry");
544784
544886
  }
544785
544887
  // =================================================================
@@ -136,6 +136,10 @@ export interface ShortcutXLModelStreamFetchTrace {
136
136
  readonly effortLevel?: ShortcutXLModelStreamEffortLevel;
137
137
  readonly speed?: 'fast' | 'standard';
138
138
  readonly thinkingType?: ShortcutXLModelStreamThinkingType;
139
+ /** Runtime run id when the host can safely correlate this fetch to an active run. */
140
+ readonly runId?: string;
141
+ /** Conversation/thread id when the host can safely correlate this fetch to a thread. */
142
+ readonly threadId?: string;
139
143
  readonly sessionId?: string;
140
144
  readonly userMessageId?: string;
141
145
  readonly messageCount?: number;
@@ -200,6 +204,12 @@ export interface ShortcutXLModelStreamErrorDetails {
200
204
  /** Client-side transport stage when the browser fetch failed outside HTTP. */
201
205
  readonly transportFailureStage?: ShortcutXLModelStreamFetchStage;
202
206
  }
207
+ export declare const INSUFFICIENT_CREDITS_ERROR_CODE = "insufficient_credits";
208
+ export declare const INSUFFICIENT_CREDITS_SAFE_MESSAGE = "Insufficient credits. Purchase more credits to continue using paid models.";
209
+ export declare const TERMINAL_MODEL_STREAM_ERROR_CODES: readonly ["insufficient_credits", "customer_credential_scope_required", "customer_credential_resolution_failed", "customer_credential_resolution_timeout", "customer_credential_resolution_cancelled", "customer_credential_unconfigured", "unsupported_credential_mode", "model_policy_rejected"];
210
+ export declare function isTerminalModelStreamErrorDetails(details: ShortcutXLModelStreamErrorDetails | undefined): boolean;
211
+ export declare function isInsufficientCreditsModelStreamFailure(details: ShortcutXLModelStreamErrorDetails | undefined, message: string | undefined): boolean;
212
+ export declare function isTerminalModelStreamFailure(details: ShortcutXLModelStreamErrorDetails | undefined, message: string | undefined): boolean;
203
213
  export interface ShortcutXLAgentAssistantMessage {
204
214
  readonly api: string;
205
215
  readonly content: readonly ShortcutXLAgentAssistantContent[];
@@ -86,6 +86,34 @@ export const ShortcutXLModelStreamFetchStage = {
86
86
  /** Response started but the SSE read failed or idle-timed out mid-stream. */
87
87
  ResponseBody: 'response_body'
88
88
  };
89
+ export const INSUFFICIENT_CREDITS_ERROR_CODE = 'insufficient_credits';
90
+ export const INSUFFICIENT_CREDITS_SAFE_MESSAGE = 'Insufficient credits. Purchase more credits to continue using paid models.';
91
+ export const TERMINAL_MODEL_STREAM_ERROR_CODES = [
92
+ INSUFFICIENT_CREDITS_ERROR_CODE,
93
+ 'customer_credential_scope_required',
94
+ 'customer_credential_resolution_failed',
95
+ 'customer_credential_resolution_timeout',
96
+ 'customer_credential_resolution_cancelled',
97
+ 'customer_credential_unconfigured',
98
+ 'unsupported_credential_mode',
99
+ 'model_policy_rejected'
100
+ ];
101
+ const TERMINAL_MODEL_STREAM_ERROR_CODE_SET = new Set(TERMINAL_MODEL_STREAM_ERROR_CODES);
102
+ export function isTerminalModelStreamErrorDetails(details) {
103
+ if (!details)
104
+ return false;
105
+ if (details.status === 402)
106
+ return true;
107
+ return details.code !== undefined && TERMINAL_MODEL_STREAM_ERROR_CODE_SET.has(details.code);
108
+ }
109
+ export function isInsufficientCreditsModelStreamFailure(details, message) {
110
+ return (details?.status === 402 ||
111
+ details?.code === INSUFFICIENT_CREDITS_ERROR_CODE ||
112
+ message === INSUFFICIENT_CREDITS_SAFE_MESSAGE);
113
+ }
114
+ export function isTerminalModelStreamFailure(details, message) {
115
+ return (isTerminalModelStreamErrorDetails(details) || message === INSUFFICIENT_CREDITS_SAFE_MESSAGE);
116
+ }
89
117
  function isRecord(value) {
90
118
  return value != null && typeof value === 'object' && !Array.isArray(value);
91
119
  }
@@ -189,8 +217,8 @@ export function networkErrorDetails(transportFailureStage) {
189
217
  };
190
218
  }
191
219
  export function safeModelStreamErrorMessage(details) {
192
- if (details?.code === 'insufficient_credits' || details?.status === 402) {
193
- return 'Insufficient credits. Purchase more credits to continue using paid models.';
220
+ if (isInsufficientCreditsModelStreamFailure(details, undefined)) {
221
+ return INSUFFICIENT_CREDITS_SAFE_MESSAGE;
194
222
  }
195
223
  if (details?.code === 'context_overflow') {
196
224
  return 'The conversation exceeded the model context window.';
@@ -1,9 +1,9 @@
1
1
  import { RunFailureCategory, RunFailureSource } from '../contracts/agent-failure.js';
2
- import { ShortcutXLModelStreamFetchStage } from '../contracts/model-stream.js';
2
+ import { INSUFFICIENT_CREDITS_ERROR_CODE, INSUFFICIENT_CREDITS_SAFE_MESSAGE, isInsufficientCreditsModelStreamFailure, ShortcutXLModelStreamFetchStage } from '../contracts/model-stream.js';
3
3
  import { classifyAgentError, getAgentErrorMessage } from './error-classification.js';
4
4
  import { getErrorDetails } from './session/session-error-recovery.js';
5
5
  const SAFE_MESSAGE_BY_CATEGORY = {
6
- [RunFailureCategory.BillingInsufficientCredits]: 'Insufficient credits. Purchase more credits to continue using paid models.',
6
+ [RunFailureCategory.BillingInsufficientCredits]: INSUFFICIENT_CREDITS_SAFE_MESSAGE,
7
7
  [RunFailureCategory.CompactionFailed]: 'Context compaction failed.',
8
8
  [RunFailureCategory.CompactionTimeout]: 'Context compaction timed out.',
9
9
  [RunFailureCategory.ContextTooLong]: 'The conversation exceeded the model context window.',
@@ -59,9 +59,6 @@ function operationKindToFailureCategory(kind, message) {
59
59
  return RunFailureCategory.LlmStreamFailed;
60
60
  }
61
61
  }
62
- function isInsufficientCreditsFailure(details) {
63
- return details?.status === 402 || details?.code === 'insufficient_credits';
64
- }
65
62
  export function createOperationFailureCause(input) {
66
63
  const category = operationKindToFailureCategory(input.opKind, input.errorMessage);
67
64
  return {
@@ -75,6 +72,17 @@ export function createOperationFailureCause(input) {
75
72
  };
76
73
  }
77
74
  function classifyModelStreamFailure(details, message) {
75
+ if (isInsufficientCreditsModelStreamFailure(details, message)) {
76
+ return {
77
+ category: RunFailureCategory.BillingInsufficientCredits,
78
+ source: RunFailureSource.Billing,
79
+ httpStatus: details?.status,
80
+ errorCode: details?.code ?? INSUFFICIENT_CREDITS_ERROR_CODE,
81
+ retryable: details?.retryable,
82
+ retryAfterMs: details?.retryAfterMs,
83
+ transportFailureStage: details?.transportFailureStage
84
+ };
85
+ }
78
86
  if (details) {
79
87
  if (details.providerErrorType === 'model_access_denied' ||
80
88
  details.code === 'model_access_denied') {
@@ -118,17 +126,6 @@ function classifyModelStreamFailure(details, message) {
118
126
  transportFailureStage: details.transportFailureStage
119
127
  };
120
128
  }
121
- if (isInsufficientCreditsFailure(details)) {
122
- return {
123
- category: RunFailureCategory.BillingInsufficientCredits,
124
- source: RunFailureSource.Billing,
125
- httpStatus: details.status,
126
- errorCode: details.code ?? 'insufficient_credits',
127
- retryable: details.retryable,
128
- retryAfterMs: details.retryAfterMs,
129
- transportFailureStage: details.transportFailureStage
130
- };
131
- }
132
129
  if (details.providerErrorType === 'network' &&
133
130
  details.transportFailureStage === ShortcutXLModelStreamFetchStage.BeforeResponse) {
134
131
  return {
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import type { AssistantMessage } from '../../ai/types.js';
21
21
  import type { RuntimeErrorRecoverySettings } from '../../contracts/agent-settings.js';
22
- import type { ShortcutXLModelStreamErrorDetails } from '../../contracts/model-stream.js';
22
+ import { type ShortcutXLModelStreamErrorDetails } from '../../contracts/model-stream.js';
23
23
  /**
24
24
  * Error categories for multi-strategy recovery.
25
25
  *
@@ -84,6 +84,22 @@ export type RuntimeAssistantMessage = AssistantMessage & {
84
84
  };
85
85
  /** Accessor for the runtime-only `errorDetails` field on a owned AI assistant message. */
86
86
  export declare function getErrorDetails(message: AssistantMessage): ShortcutXLModelStreamErrorDetails | undefined;
87
+ export type ErrorClassificationDisposition = {
88
+ kind: 'none';
89
+ } | {
90
+ kind: 'terminal';
91
+ } | {
92
+ kind: 'categorized';
93
+ category: ErrorCategory;
94
+ };
95
+ /**
96
+ * Classify an assistant message into an explicit recovery disposition.
97
+ *
98
+ * MUST remain synchronous — called from the sync _handleAgentLoopEvent handler
99
+ * before async queue processing, to ensure the recovery promise exists
100
+ * before waitForRetry() is called.
101
+ */
102
+ export declare function classifyErrorDisposition(message: AssistantMessage, isContextOverflow: ContextOverflowChecker): ErrorClassificationDisposition;
87
103
  /**
88
104
  * Classify an error into a recovery category.
89
105
  *
@@ -141,6 +157,10 @@ export declare class SessionErrorRecovery {
141
157
  private _continueRecovery;
142
158
  /** Reset all state to idle. */
143
159
  private _reset;
160
+ /** Emit a cancellation event once for an active, visible recovery. */
161
+ private _emitCancelledRecovery;
162
+ /** Cancel recovery state and any in-progress backoff sleep. */
163
+ private _cancelRecovery;
144
164
  /**
145
165
  * Create the recovery promise synchronously when an agent_end event arrives.
146
166
  *
@@ -178,6 +198,14 @@ export declare class SessionErrorRecovery {
178
198
  * Resets state and resolves the promise.
179
199
  */
180
200
  onSuccess(): void;
201
+ /**
202
+ * Called when a continuation turn reaches an unrecoverable assistant error.
203
+ * This resolves the in-flight recovery as failed instead of treating the
204
+ * terminal, non-retryable failure as a successful retry.
205
+ */
206
+ onTerminalFailure(finalError: string | undefined): void;
207
+ /** Settle recovery when a continuation agent loop reaches agent_end. */
208
+ settleAgentEnd(lastAssistant: AssistantMessage | undefined, isContextOverflow: ContextOverflowChecker): void;
181
209
  /** Cancel in-progress recovery. */
182
210
  abort(): void;
183
211
  /** Wait for any in-progress recovery to complete. */
@@ -17,6 +17,7 @@
17
17
  * classifyError + promise creation happens synchronously in _handleAgentLoopEvent,
18
18
  * strategy dispatch happens asynchronously in the queued _processAgentLoopEvent.
19
19
  */
20
+ import { isTerminalModelStreamErrorDetails, isTerminalModelStreamFailure } from '../../contracts/model-stream.js';
20
21
  import { sleep } from '../../utils/sleep.js';
21
22
  /** Accessor for the runtime-only `errorDetails` field on a owned AI assistant message. */
22
23
  export function getErrorDetails(message) {
@@ -43,23 +44,10 @@ const SERVER_ERROR_PATTERN = /\b500\b|\b502\b|\b503\b|\b504\b|\b524\b|service.?u
43
44
  // - Chrome/Node: "Failed to fetch", "network error", and the
44
45
  // already-listed connection/reset strings
45
46
  const CONNECTION_ERROR_PATTERN = /connection.?error|connection.?refused|other side closed|fetch failed|failed to fetch|upstream.?connect|reset before headers|terminated|retry delay|peer closed|complete message body|load failed|network.?error|internet connection appears to be offline/i;
46
- const TERMINAL_GATEWAY_ERROR_CODES = new Set([
47
- 'insufficient_credits',
48
- 'customer_credential_scope_required',
49
- 'customer_credential_resolution_failed',
50
- 'customer_credential_resolution_timeout',
51
- 'customer_credential_resolution_cancelled',
52
- 'customer_credential_unconfigured',
53
- 'unsupported_credential_mode',
54
- 'model_policy_rejected'
55
- ]);
56
47
  function classifyStructuredError(details) {
57
48
  if (!details)
58
49
  return null;
59
- if (details.code !== undefined && TERMINAL_GATEWAY_ERROR_CODES.has(details.code)) {
60
- return 'terminal';
61
- }
62
- if (details.status === 402)
50
+ if (isTerminalModelStreamErrorDetails(details))
63
51
  return 'terminal';
64
52
  if (details.providerErrorType === 'unauthorized' || details.status === 401)
65
53
  return 'auth_error';
@@ -78,45 +66,59 @@ function classifyStructuredError(details) {
78
66
  return null;
79
67
  }
80
68
  /**
81
- * Classify an error into a recovery category.
69
+ * Classify an assistant message into an explicit recovery disposition.
82
70
  *
83
71
  * MUST remain synchronous — called from the sync _handleAgentLoopEvent handler
84
72
  * before async queue processing, to ensure the recovery promise exists
85
73
  * before waitForRetry() is called.
86
- *
87
- * @returns ErrorCategory if error is classifiable, null if not an error
88
74
  */
89
- export function classifyError(message, isContextOverflow) {
75
+ export function classifyErrorDisposition(message, isContextOverflow) {
90
76
  if (message.stopReason !== 'error')
91
- return null;
77
+ return { kind: 'none' };
92
78
  if (!message.errorMessage)
93
- return null;
79
+ return { kind: 'none' };
94
80
  // Context overflow has highest priority — exclusive path
95
81
  if (isContextOverflow(message))
96
- return 'context_overflow';
97
- const structuredCategory = classifyStructuredError(getErrorDetails(message));
82
+ return { kind: 'categorized', category: 'context_overflow' };
83
+ const details = getErrorDetails(message);
84
+ const structuredCategory = classifyStructuredError(details);
98
85
  if (structuredCategory === 'terminal') {
99
- return null;
86
+ return { kind: 'terminal' };
100
87
  }
101
88
  if (structuredCategory)
102
- return structuredCategory;
89
+ return { kind: 'categorized', category: structuredCategory };
103
90
  const err = message.errorMessage;
91
+ if (isTerminalModelStreamFailure(details, err))
92
+ return { kind: 'terminal' };
104
93
  // Auth errors — must check before server errors (401 overlaps)
105
94
  if (AUTH_ERROR_PATTERN.test(err))
106
- return 'auth_error';
95
+ return { kind: 'categorized', category: 'auth_error' };
107
96
  // Rate limits
108
97
  if (RATE_LIMIT_PATTERN.test(err))
109
- return 'rate_limit';
98
+ return { kind: 'categorized', category: 'rate_limit' };
110
99
  // Overloaded
111
100
  if (OVERLOADED_PATTERN.test(err))
112
- return 'overloaded';
101
+ return { kind: 'categorized', category: 'overloaded' };
113
102
  // Server errors
114
103
  if (SERVER_ERROR_PATTERN.test(err))
115
- return 'server_error';
104
+ return { kind: 'categorized', category: 'server_error' };
116
105
  // Connection errors → server_error (transient infrastructure)
117
106
  if (CONNECTION_ERROR_PATTERN.test(err))
118
- return 'server_error';
119
- return 'unknown';
107
+ return { kind: 'categorized', category: 'server_error' };
108
+ return { kind: 'categorized', category: 'unknown' };
109
+ }
110
+ /**
111
+ * Classify an error into a recovery category.
112
+ *
113
+ * MUST remain synchronous — called from the sync _handleAgentLoopEvent handler
114
+ * before async queue processing, to ensure the recovery promise exists
115
+ * before waitForRetry() is called.
116
+ *
117
+ * @returns ErrorCategory if error is classifiable, null if not an error
118
+ */
119
+ export function classifyError(message, isContextOverflow) {
120
+ const disposition = classifyErrorDisposition(message, isContextOverflow);
121
+ return disposition.kind === 'categorized' ? disposition.category : null;
120
122
  }
121
123
  /**
122
124
  * Server-error retry budget. 5 attempts × exponential backoff (1s, 2s, 4s,
@@ -238,20 +240,26 @@ export class SessionErrorRecovery {
238
240
  _emit(event) {
239
241
  this._listener?.(event);
240
242
  }
241
- async _continueRecovery(category, attempt, actions) {
242
- try {
243
- // Keep the recovery promise armed until the continuation turn emits its
244
- // own terminal agent_end. Otherwise callers can submit into a still-live
245
- // retry and trip the "previous turn is still streaming" guard.
246
- //
247
- // Contract: `actions.continue` is wired in compose.ts to `agent.continue()`,
248
- // which resolves only after the next turn reaches agent_end (or rejects on
249
- // failure). See `createEmbeddedRecoveryWiring` callsite in
250
- // `embedded-agent/compose.ts`.
251
- await actions.continue();
252
- return true;
253
- }
254
- catch (error) {
243
+ _continueRecovery(category, attempt, actions) {
244
+ // Launch the continuation without awaiting it here. CLI AgentSession calls
245
+ // handleError() from its serialized event queue; awaiting agent.continue()
246
+ // would hold that queue until the entire retry finishes, starving the
247
+ // retry's own agent_start/message/tool events and leaving the UI frozen on
248
+ // the recovery loader. The recovery promise remains armed until the
249
+ // continuation's agent_end is processed by settleAgentEnd().
250
+ const activeRecovery = this._promise;
251
+ void Promise.resolve()
252
+ .then(() => {
253
+ // Cancellation can race the end of the backoff delay. Re-check before
254
+ // starting so an already-cancelled retry cannot launch invisibly.
255
+ if (!activeRecovery || this._promise !== activeRecovery)
256
+ return;
257
+ return actions.continue();
258
+ })
259
+ .catch((error) => {
260
+ // Ignore completion from a continuation that was cancelled or replaced.
261
+ if (!activeRecovery || this._promise !== activeRecovery)
262
+ return;
255
263
  this._emit({
256
264
  type: 'recovery_end',
257
265
  category,
@@ -260,17 +268,38 @@ export class SessionErrorRecovery {
260
268
  finalError: error instanceof Error ? error.message : String(error)
261
269
  });
262
270
  this._reset();
263
- return false;
264
- }
271
+ });
272
+ return true;
265
273
  }
266
274
  /** Reset all state to idle. */
267
275
  _reset() {
276
+ this._abortController = undefined;
268
277
  this._attempt = 0;
269
278
  this._category = null;
270
279
  this._resolve?.();
271
280
  this._resolve = undefined;
272
281
  this._promise = undefined;
273
282
  }
283
+ /** Emit a cancellation event once for an active, visible recovery. */
284
+ _emitCancelledRecovery() {
285
+ if (!this._promise || this._attempt <= 0 || !this._category)
286
+ return;
287
+ this._emit({
288
+ type: 'recovery_end',
289
+ category: this._category,
290
+ success: false,
291
+ attempt: this._attempt,
292
+ finalError: 'Recovery cancelled'
293
+ });
294
+ }
295
+ /** Cancel recovery state and any in-progress backoff sleep. */
296
+ _cancelRecovery() {
297
+ const abortController = this._abortController;
298
+ this._abortController = undefined;
299
+ this._emitCancelledRecovery();
300
+ this._reset();
301
+ abortController?.abort();
302
+ }
274
303
  /**
275
304
  * Create the recovery promise synchronously when an agent_end event arrives.
276
305
  *
@@ -314,6 +343,15 @@ export class SessionErrorRecovery {
314
343
  const config = STRATEGY_CONFIGS[category];
315
344
  // No retry for non-retryable categories
316
345
  if (config.maxAttempts === 0) {
346
+ if (this._attempt > 0 || this._promise) {
347
+ this._emit({
348
+ type: 'recovery_end',
349
+ category: this._category ?? category,
350
+ success: false,
351
+ attempt: this._attempt,
352
+ finalError: message.errorMessage
353
+ });
354
+ }
317
355
  this._reset();
318
356
  return false;
319
357
  }
@@ -448,6 +486,10 @@ export class SessionErrorRecovery {
448
486
  }
449
487
  /** Handle abort during sleep. */
450
488
  _handleAbort(category) {
489
+ // abort() emits and resets synchronously. If the aborted sleep rejects
490
+ // afterward, do not emit a second recovery_end event.
491
+ if (!this._promise)
492
+ return false;
451
493
  const attempt = this._attempt;
452
494
  this._abortController = undefined;
453
495
  this._emit({
@@ -476,10 +518,39 @@ export class SessionErrorRecovery {
476
518
  });
477
519
  this._reset();
478
520
  }
521
+ /**
522
+ * Called when a continuation turn reaches an unrecoverable assistant error.
523
+ * This resolves the in-flight recovery as failed instead of treating the
524
+ * terminal, non-retryable failure as a successful retry.
525
+ */
526
+ onTerminalFailure(finalError) {
527
+ if (this._attempt <= 0)
528
+ return;
529
+ const category = this._category ?? 'unknown';
530
+ this._emit({
531
+ type: 'recovery_end',
532
+ category,
533
+ success: false,
534
+ attempt: this._attempt,
535
+ finalError
536
+ });
537
+ this._reset();
538
+ }
539
+ /** Settle recovery when a continuation agent loop reaches agent_end. */
540
+ settleAgentEnd(lastAssistant, isContextOverflow) {
541
+ if (this._attempt <= 0)
542
+ return;
543
+ if (!lastAssistant || lastAssistant.stopReason !== 'error') {
544
+ this.onSuccess();
545
+ return;
546
+ }
547
+ if (classifyErrorDisposition(lastAssistant, isContextOverflow).kind === 'terminal') {
548
+ this.onTerminalFailure(lastAssistant.errorMessage);
549
+ }
550
+ }
479
551
  /** Cancel in-progress recovery. */
480
552
  abort() {
481
- this._abortController?.abort();
482
- this._reset();
553
+ this._cancelRecovery();
483
554
  }
484
555
  /** Wait for any in-progress recovery to complete. */
485
556
  async wait() {
@@ -362,9 +362,11 @@ function toShortcutXLErrorReason(stopReason) {
362
362
  : ShortcutXLModelStreamStopReason.Error;
363
363
  }
364
364
  function toShortcutXLAssistantMessage(message) {
365
+ const errorDetails = message.errorDetails;
365
366
  return {
366
367
  api: message.api,
367
368
  content: message.content.map((content) => toShortcutXLAssistantContent(content)),
369
+ ...(errorDetails ? { errorDetails } : {}),
368
370
  errorMessage: message.errorMessage,
369
371
  model: message.model,
370
372
  provider: message.provider,
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { ACTION_TOOL_NAMES } from './action-tool-names.js';
7
7
  import { buildHostToolList } from './build-tool-list.js';
8
- import { BASH_COMMAND, EXECUTE_CODE, EXECUTE_TOOL, GET_TOOL_INFO, MCP, READ_SKILL, SEND_MESSAGE, SWITCH_MODE, TAKE_SCREENSHOT, TASK } from './tool-names.js';
8
+ import { BASH_COMMAND, EXECUTE_CODE, EXECUTE_TOOL, GET_TOOL_INFO, MCP, MODIFY_SKILL, READ_SKILL, SEND_MESSAGE, SWITCH_MODE, TAKE_SCREENSHOT, TASK } from './tool-names.js';
9
9
  /** First-class tool names exposed to the LLM per agent mode. */
10
10
  export const TOOLS_BY_MODE_NAMES = {
11
11
  action: ACTION_TOOL_NAMES,
@@ -17,6 +17,7 @@ export const TOOLS_BY_MODE_NAMES = {
17
17
  TAKE_SCREENSHOT,
18
18
  MCP,
19
19
  READ_SKILL,
20
+ MODIFY_SKILL,
20
21
  TASK,
21
22
  SEND_MESSAGE
22
23
  ],
@@ -14,6 +14,7 @@ export declare const ShortcutXLAgentModelId: {
14
14
  readonly ClaudeFable5: "claude-fable-5";
15
15
  readonly ClaudeOpus48: "claude-opus-4-8";
16
16
  readonly Glm52Fireworks: "accounts/fireworks/models/glm-5p2";
17
+ readonly Grok45: "grok-4.5";
17
18
  readonly OpenAIReasoning: "gpt-5.6-sol";
18
19
  /** Low-cost Shortcut-hosted model. */
19
20
  readonly Hosted: "pivot";
@@ -9,6 +9,7 @@ export const ShortcutXLAgentModelId = {
9
9
  ClaudeFable5: SHORTCUT_MODEL_ID.ClaudeFable5,
10
10
  ClaudeOpus48: SHORTCUT_MODEL_ID.ClaudeOpus48,
11
11
  Glm52Fireworks: SHORTCUT_MODEL_ID.Glm52Fireworks,
12
+ Grok45: SHORTCUT_MODEL_ID.Grok45,
12
13
  OpenAIReasoning: SHORTCUT_OPENAI_MODEL_ID,
13
14
  /** Low-cost Shortcut-hosted model. */
14
15
  Hosted: SHORTCUT_MODEL_ID.Hosted
@@ -1,5 +1,5 @@
1
1
  import { isContextOverflow } from '../core/session/context-overflow.js';
2
- import { classifyError, SessionErrorRecovery } from '../core/session/session-error-recovery.js';
2
+ import { classifyErrorDisposition, SessionErrorRecovery } from '../core/session/session-error-recovery.js';
3
3
  function findLastAssistant(messages) {
4
4
  for (let i = messages.length - 1; i >= 0; i -= 1) {
5
5
  const message = messages[i];
@@ -31,9 +31,14 @@ function removeLastErrorGroup(agent, sessionStore, funnel) {
31
31
  /** Build the embedded equivalent of CLI `SessionErrorRecovery`. */
32
32
  export function createEmbeddedRecoveryWiring(options) {
33
33
  const recovery = new SessionErrorRecovery({ sleep: options.recoverySleep });
34
- if (options.onRecoveryEvent) {
35
- recovery.onEvent(options.onRecoveryEvent);
36
- }
34
+ let recoverySucceeded;
35
+ recovery.onEvent((event) => {
36
+ if (event.type === 'recovery_start')
37
+ recoverySucceeded = undefined;
38
+ if (event.type === 'recovery_end')
39
+ recoverySucceeded = event.success;
40
+ options.onRecoveryEvent?.(event);
41
+ });
37
42
  const isOverflow = (message) => isContextOverflow(message, options.agent.state.model?.contextWindow ?? 0);
38
43
  return {
39
44
  abort() {
@@ -46,8 +51,12 @@ export function createEmbeddedRecoveryWiring(options) {
46
51
  // Embedded context-overflow recovery is already owned by compactionWiring,
47
52
  // which can compact persisted entries and continue the agent. Do not arm
48
53
  // SessionErrorRecovery for the same event or the two systems race.
49
- if (lastAssistant && classifyError(lastAssistant, isOverflow) === 'context_overflow')
50
- return;
54
+ if (lastAssistant) {
55
+ const disposition = classifyErrorDisposition(lastAssistant, isOverflow);
56
+ if (disposition.kind === 'categorized' && disposition.category === 'context_overflow') {
57
+ return;
58
+ }
59
+ }
51
60
  recovery.prepareForAgentEnd(lastAssistant, isOverflow, options.getSettings());
52
61
  },
53
62
  async handleAgentEnd(event) {
@@ -59,20 +68,33 @@ export function createEmbeddedRecoveryWiring(options) {
59
68
  recovery.onSuccess();
60
69
  return false;
61
70
  }
62
- const category = classifyError(lastAssistant, isOverflow);
63
- if (category === 'context_overflow')
71
+ const disposition = classifyErrorDisposition(lastAssistant, isOverflow);
72
+ if (disposition.kind === 'categorized' && disposition.category === 'context_overflow') {
64
73
  return false;
65
- if (!category) {
74
+ }
75
+ if (disposition.kind === 'none') {
66
76
  if (recovery.attempt > 0)
67
77
  recovery.onSuccess();
68
78
  return false;
69
79
  }
70
- return recovery.handleError(lastAssistant, category, options.getSettings(), {
80
+ if (disposition.kind === 'terminal') {
81
+ if (recovery.attempt > 0)
82
+ recovery.onTerminalFailure(lastAssistant.errorMessage);
83
+ return false;
84
+ }
85
+ const initiated = await recovery.handleError(lastAssistant, disposition.category, options.getSettings(), {
71
86
  removeLastAssistantMessage: () => removeLastErrorGroup(options.agent, options.sessionStore, options.funnel),
72
87
  continue: options.continueAgent,
73
88
  runCompaction: async () => false,
74
89
  switchToFallbackModel: async () => false
75
90
  });
91
+ // Embedded recovery runs outside a serialized agent-event queue, so it
92
+ // can retain its completion-ordering contract without starving events.
93
+ // CLI AgentSession instead relies on recovery.wait() from prompt().
94
+ if (!initiated)
95
+ return false;
96
+ await recovery.wait();
97
+ return recoverySucceeded === true;
76
98
  },
77
99
  wait() {
78
100
  return recovery.wait();
@@ -4,6 +4,7 @@ export declare const SHORTCUT_MODEL_ID: {
4
4
  readonly ClaudeOpus48: "claude-opus-4-8";
5
5
  readonly Glm52Fireworks: "accounts/fireworks/models/glm-5p2";
6
6
  readonly Gpt56Sol: "gpt-5.6-sol";
7
+ readonly Grok45: "grok-4.5";
7
8
  readonly Hosted: "pivot";
8
9
  readonly Gemini3Flash: "gemini-3-flash-preview";
9
10
  readonly ClaudeHaiku45: "claude-haiku-4-5-20251001";
@@ -13,6 +14,7 @@ export declare const SHORTCUT_MODEL_REF: {
13
14
  readonly ClaudeOpus48: "shortcut/claude-opus-4-8";
14
15
  readonly Glm52Fireworks: "shortcut/accounts/fireworks/models/glm-5p2";
15
16
  readonly Gpt56Sol: "shortcut/gpt-5.6-sol";
17
+ readonly Grok45: "shortcut/grok-4.5";
16
18
  readonly Hosted: "shortcut/pivot";
17
19
  };
18
20
  export declare const SHORTCUT_OPENAI_MODEL_ID: "gpt-5.6-sol";
package/dist/model-ids.js CHANGED
@@ -4,6 +4,7 @@ export const SHORTCUT_MODEL_ID = {
4
4
  ClaudeOpus48: 'claude-opus-4-8',
5
5
  Glm52Fireworks: 'accounts/fireworks/models/glm-5p2',
6
6
  Gpt56Sol: 'gpt-5.6-sol',
7
+ Grok45: 'grok-4.5',
7
8
  Hosted: 'pivot',
8
9
  Gemini3Flash: 'gemini-3-flash-preview',
9
10
  ClaudeHaiku45: 'claude-haiku-4-5-20251001'
@@ -13,6 +14,7 @@ export const SHORTCUT_MODEL_REF = {
13
14
  ClaudeOpus48: `${SHORTCUT_PROVIDER_ID}/${SHORTCUT_MODEL_ID.ClaudeOpus48}`,
14
15
  Glm52Fireworks: `${SHORTCUT_PROVIDER_ID}/${SHORTCUT_MODEL_ID.Glm52Fireworks}`,
15
16
  Gpt56Sol: `${SHORTCUT_PROVIDER_ID}/${SHORTCUT_MODEL_ID.Gpt56Sol}`,
17
+ Grok45: `${SHORTCUT_PROVIDER_ID}/${SHORTCUT_MODEL_ID.Grok45}`,
16
18
  Hosted: `${SHORTCUT_PROVIDER_ID}/${SHORTCUT_MODEL_ID.Hosted}`
17
19
  };
18
20
  export const SHORTCUT_OPENAI_MODEL_ID = SHORTCUT_MODEL_ID.Gpt56Sol;
@@ -246,7 +246,7 @@ export async function runRpcMode(session) {
246
246
  return success(id, 'set_auto_retry');
247
247
  }
248
248
  case 'abort_retry': {
249
- session.abortRetry();
249
+ await session.abortRetry();
250
250
  return success(id, 'abort_retry');
251
251
  }
252
252
  // =================================================================
@@ -1,4 +1,3 @@
1
- export declare function readFilePrefix(filePath: string, maxBytes?: number): Promise<Buffer>;
2
1
  export declare function detectFileMimeTypeFromBuffer(sample: Uint8Array): Promise<string | null>;
3
2
  export declare function isLikelyUtf8TextFile(options: {
4
3
  sample: Uint8Array;
@@ -1,19 +1,6 @@
1
1
  import { fileTypeFromBuffer } from 'file-type';
2
- import { open } from 'node:fs/promises';
3
2
  import { TextDecoder } from 'node:util';
4
- const FILE_TYPE_SNIFF_BYTES = 8192;
5
3
  const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
6
- export async function readFilePrefix(filePath, maxBytes = FILE_TYPE_SNIFF_BYTES) {
7
- const fileHandle = await open(filePath, 'r');
8
- try {
9
- const buffer = Buffer.alloc(maxBytes);
10
- const { bytesRead } = await fileHandle.read(buffer, 0, maxBytes, 0);
11
- return buffer.subarray(0, bytesRead);
12
- }
13
- finally {
14
- await fileHandle.close();
15
- }
16
- }
17
4
  export async function detectFileMimeTypeFromBuffer(sample) {
18
5
  const detected = sample.length > 0 ? await fileTypeFromBuffer(sample) : undefined;
19
6
  return detected?.mime ?? null;
@@ -2529,7 +2529,7 @@ export class InteractiveMode {
2529
2529
  handleRecoveryStartEvent(event) {
2530
2530
  this.retryEscapeHandler = this.defaultEditor.onEscape;
2531
2531
  this.defaultEditor.onEscape = () => {
2532
- this.session.abortRetry();
2532
+ void this.session.abortRetry();
2533
2533
  };
2534
2534
  const retryChildren = this.chatContainer.children;
2535
2535
  for (let i = retryChildren.length - 1; i >= 0; i--) {
@@ -2556,7 +2556,10 @@ export class InteractiveMode {
2556
2556
  this.retryLoader = undefined;
2557
2557
  this.statusContainer.clear();
2558
2558
  }
2559
- if (!event.success) {
2559
+ if (!event.success && event.finalError === 'Recovery cancelled') {
2560
+ this.showStatus('Recovery cancelled');
2561
+ }
2562
+ else if (!event.success) {
2560
2563
  this.showError(`Recovery failed after ${event.attempt} attempts: ${event.finalError || 'Unknown error'}`);
2561
2564
  }
2562
2565
  this.ui.requestRender();
@@ -1,5 +1,5 @@
1
1
  import type { Api, Model } from './ai/types.js';
2
- export type ShortcutModelDefinition = Pick<Model<Api>, 'id' | 'name' | 'reasoning' | 'input' | 'cost' | 'contextWindow' | 'maxTokens'>;
2
+ export type ShortcutModelDefinition = Pick<Model<Api>, 'id' | 'name' | 'reasoning' | 'input' | 'cost' | 'contextWindow' | 'maxTokens' | 'compat'>;
3
3
  export declare const DEFAULT_SHORTCUT_MODEL_ID: "gpt-5.6-sol";
4
4
  export declare function createShortcutPublicModelDefinitions(): ShortcutModelDefinition[];
5
5
  export declare function createShortcutHostedModelDefinitions(): ShortcutModelDefinition[];
@@ -28,6 +28,21 @@ const SHORTCUT_PUBLIC_MODEL_DEFINITIONS = [
28
28
  contextWindow: 1_000_000,
29
29
  maxTokens: 128000
30
30
  },
31
+ {
32
+ id: SHORTCUT_MODEL_ID.Grok45,
33
+ name: 'Grok 4.5',
34
+ reasoning: true,
35
+ input: ['text', 'image'],
36
+ cost: { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
37
+ contextWindow: 500_000,
38
+ maxTokens: 128000,
39
+ compat: {
40
+ supportsStore: false,
41
+ supportsDeveloperRole: false,
42
+ supportsReasoningEffort: true,
43
+ maxTokensField: 'max_completion_tokens'
44
+ }
45
+ },
31
46
  {
32
47
  id: SHORTCUT_MODEL_ID.Glm52Fireworks,
33
48
  name: 'GLM 5.2',
@@ -75,7 +90,8 @@ function cloneShortcutModelDefinition(model) {
75
90
  return {
76
91
  ...model,
77
92
  input: [...model.input],
78
- cost: { ...model.cost }
93
+ cost: { ...model.cost },
94
+ ...(model.compat ? { compat: { ...model.compat } } : {})
79
95
  };
80
96
  }
81
97
  export function createShortcutPublicModelDefinitions() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shortcutxl",
3
- "version": "0.3.70",
3
+ "version": "0.3.71",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
Binary file