@ccpocket/bridge 1.72.2 → 1.72.3

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.
@@ -89,6 +89,7 @@ export function hasExplicitClaudeCredential(env = process.env) {
89
89
  function canStartClaudeSdk(env = process.env) {
90
90
  return hasExplicitClaudeCredential(env) || isClaudeOAuthOptInEnabled(env);
91
91
  }
92
+ const CLAUDE_AUTH_RESOLUTION_TIMEOUT_MS = 500;
92
93
  export async function listAvailableClaudeModels(projectPath) {
93
94
  if (!canStartClaudeSdk()) {
94
95
  throw new Error(CLAUDE_OAUTH_OPT_IN_MESSAGE);
@@ -260,6 +261,63 @@ export function buildAskUserAnswers(input, result) {
260
261
  export function resolvePermissionMode(current, requested) {
261
262
  return requested ?? current;
262
263
  }
264
+ const CLAUDE_SYSTEM_INJECTED_USER_TEXT = /^<(?:local-command-caveat|local-command-std(?:err|out)|task-notification|teammate-message|bash-(?:input|stdout))>/;
265
+ const UNKNOWN_CLAUDE_ASSISTANT_ERROR = {
266
+ message: "Claude stopped because of an unknown request error.",
267
+ errorCode: "claude_assistant_error",
268
+ };
269
+ const CLAUDE_ASSISTANT_ERRORS = {
270
+ authentication_failed: {
271
+ message: "Claude authentication failed. Sign in again on the Bridge machine.",
272
+ errorCode: "auth_token_expired",
273
+ },
274
+ oauth_org_not_allowed: {
275
+ message: "This Claude subscription organization cannot be used by the Agent SDK.",
276
+ errorCode: "claude_oauth_org_not_allowed",
277
+ },
278
+ billing_error: {
279
+ message: "Claude could not continue because of an account billing error.",
280
+ errorCode: "claude_billing_error",
281
+ },
282
+ rate_limit: {
283
+ message: "Claude is temporarily rate limited. Try again shortly.",
284
+ errorCode: "claude_rate_limit",
285
+ },
286
+ invalid_request: {
287
+ message: "Claude rejected this request as invalid.",
288
+ errorCode: "claude_invalid_request",
289
+ },
290
+ model_not_found: {
291
+ message: "The selected Claude model is not available for this account.",
292
+ errorCode: "claude_model_not_found",
293
+ },
294
+ server_error: {
295
+ message: "Claude encountered a server error.",
296
+ errorCode: "claude_server_error",
297
+ },
298
+ unknown: UNKNOWN_CLAUDE_ASSISTANT_ERROR,
299
+ max_output_tokens: {
300
+ message: "Claude reached the maximum response length before finishing.",
301
+ errorCode: "claude_max_output_tokens",
302
+ },
303
+ };
304
+ function claudeAssistantErrorMessage(error) {
305
+ if (typeof error !== "string" || error.length === 0)
306
+ return null;
307
+ const details = Object.hasOwn(CLAUDE_ASSISTANT_ERRORS, error)
308
+ ? CLAUDE_ASSISTANT_ERRORS[error]
309
+ : UNKNOWN_CLAUDE_ASSISTANT_ERROR;
310
+ return { type: "error", ...details };
311
+ }
312
+ function isInternalClaudeResultError(error) {
313
+ return (error.startsWith("[ede_diagnostic]") ||
314
+ error.includes("Bun is not defined"));
315
+ }
316
+ function isClaudeSystemInjectedUserText(text) {
317
+ const normalized = text.trimStart();
318
+ return (CLAUDE_SYSTEM_INJECTED_USER_TEXT.test(normalized) ||
319
+ normalized.startsWith("Base directory for this skill:"));
320
+ }
263
321
  /**
264
322
  * Convert SDK messages to the ServerMessage format used by the WebSocket protocol.
265
323
  * Exported for testing.
@@ -281,13 +339,39 @@ export function sdkMessageToServerMessage(msg) {
281
339
  if (sys.subtype === "compact_boundary") {
282
340
  return { type: "status", status: "compacting" };
283
341
  }
342
+ if (sys.subtype === "status" && sys.compact_result === "failed") {
343
+ const compactError = typeof sys.compact_error === "string" ? sys.compact_error.trim() : "";
344
+ return {
345
+ type: "error",
346
+ message: compactError
347
+ ? `Claude context compaction failed: ${compactError}`
348
+ : "Claude context compaction failed.",
349
+ errorCode: "claude_compaction_failed",
350
+ };
351
+ }
284
352
  return null;
285
353
  }
286
354
  case "assistant": {
287
355
  const ast = msg;
356
+ const rawContent = ast.message.content;
357
+ const content = Array.isArray(rawContent)
358
+ ? rawContent.filter((block) => {
359
+ if (!block || typeof block !== "object")
360
+ return true;
361
+ const candidate = block;
362
+ return !(candidate.type === "thinking" &&
363
+ typeof candidate.thinking === "string" &&
364
+ candidate.thinking.trim().length === 0);
365
+ })
366
+ : rawContent;
367
+ if (Array.isArray(content) && content.length === 0)
368
+ return null;
288
369
  return {
289
370
  type: "assistant",
290
- message: ast.message,
371
+ message: {
372
+ ...ast.message,
373
+ content,
374
+ },
291
375
  ...(ast.uuid ? { messageUuid: ast.uuid } : {}),
292
376
  };
293
377
  }
@@ -317,11 +401,13 @@ export function sdkMessageToServerMessage(msg) {
317
401
  .filter((c) => c.type === "text")
318
402
  .map((c) => c.text);
319
403
  if (texts.length > 0) {
404
+ const isSynthetic = usr.isSynthetic === true ||
405
+ texts.every(isClaudeSystemInjectedUserText);
320
406
  return {
321
407
  type: "user_input",
322
408
  text: texts.join("\n"),
323
409
  ...(usr.uuid ? { userMessageUuid: usr.uuid } : {}),
324
- ...(usr.isSynthetic ? { isSynthetic: true } : {}),
410
+ ...(isSynthetic ? { isSynthetic: true } : {}),
325
411
  ...(usr.isMeta ? { isMeta: true } : {}),
326
412
  };
327
413
  }
@@ -342,12 +428,19 @@ export function sdkMessageToServerMessage(msg) {
342
428
  ...tokenUsage,
343
429
  };
344
430
  }
345
- // All other result subtypes are errors
346
- const errorText = Array.isArray(res.errors) ? res.errors.join("\n") : "Unknown error";
347
- // Suppress spurious CLI runtime errors (SDK bug: Bun API referenced on Node.js)
348
- if (errorText.includes("Bun is not defined")) {
431
+ // Claude Code can misclassify a routine interrupt as
432
+ // error_during_execution and prepend an internal-only EDE diagnostic.
433
+ // Its own terminal renderer filters these records, so keep the same
434
+ // boundary here while preserving any accompanying real error.
435
+ const rawErrors = Array.isArray(res.errors)
436
+ ? res.errors.filter((error) => typeof error === "string")
437
+ : [];
438
+ const visibleErrors = rawErrors.filter((error) => !isInternalClaudeResultError(error));
439
+ if (rawErrors.length > 0 && visibleErrors.length === 0) {
349
440
  return null;
350
441
  }
442
+ // All other result subtypes are errors
443
+ const errorText = visibleErrors.length > 0 ? visibleErrors.join("\n") : "Unknown error";
351
444
  return {
352
445
  type: "result",
353
446
  subtype: "error",
@@ -394,6 +487,10 @@ export class SdkProcess extends EventEmitter {
394
487
  get permissionMode() { return this._permissionMode; }
395
488
  _model;
396
489
  get model() { return this._model; }
490
+ authClassification = "unknown";
491
+ authGeneration = 0;
492
+ authResolution = Promise.resolve();
493
+ authResolutionPending = false;
397
494
  sessionAllowRules = new Set();
398
495
  initTimeoutId = null;
399
496
  sessionEndEmitted = false;
@@ -404,6 +501,7 @@ export class SdkProcess extends EventEmitter {
404
501
  _projectPath = null;
405
502
  toolCallsSinceLastResult = 0;
406
503
  fileEditsSinceLastResult = 0;
504
+ pendingAssistantError = null;
407
505
  launchStartedAt = 0;
408
506
  get status() {
409
507
  return this._status;
@@ -432,6 +530,10 @@ export class SdkProcess extends EventEmitter {
432
530
  }
433
531
  this.stopped = false;
434
532
  this._sessionId = null;
533
+ this.authGeneration += 1;
534
+ this.authClassification = "unknown";
535
+ this.authResolution = Promise.resolve();
536
+ this.authResolutionPending = false;
435
537
  this.sessionEndEmitted = false;
436
538
  this.pendingPermissions.clear();
437
539
  this.permissionModeGeneration += 1;
@@ -440,6 +542,7 @@ export class SdkProcess extends EventEmitter {
440
542
  this.sessionAllowRules.clear();
441
543
  this.toolCallsSinceLastResult = 0;
442
544
  this.fileEditsSinceLastResult = 0;
545
+ this.pendingAssistantError = null;
443
546
  this.launchStartedAt = Date.now();
444
547
  if (options?.initialInput) {
445
548
  this.pendingInputQueue.push({ text: options.initialInput });
@@ -528,14 +631,19 @@ export class SdkProcess extends EventEmitter {
528
631
  },
529
632
  },
530
633
  });
634
+ const queryInstance = this.queryInstance;
635
+ const authGeneration = this.authGeneration;
636
+ this.authResolution = this.resolveAuthClassification(queryInstance, authGeneration);
531
637
  // Background message processing
532
- this.processMessages().catch((err) => {
533
- if (this.stopped) {
638
+ this.processMessages(authGeneration, this.authResolution).catch((err) => {
639
+ if (this.stopped || authGeneration !== this.authGeneration) {
534
640
  // Suppress errors from intentional stop (SDK bug: Bun API referenced on Node.js)
535
641
  return;
536
642
  }
537
643
  console.error("[sdk-process] Message processing error:", err);
538
- this.emitMessage({ type: "error", message: `SDK error: ${err instanceof Error ? err.message : String(err)}` });
644
+ if (!this.flushPendingAssistantError()) {
645
+ this.emitMessage({ type: "error", message: `SDK error: ${err instanceof Error ? err.message : String(err)}` });
646
+ }
539
647
  this.stop();
540
648
  this.emit("exit", 1);
541
649
  });
@@ -548,6 +656,10 @@ export class SdkProcess extends EventEmitter {
548
656
  this.initTimeoutId = null;
549
657
  }
550
658
  this.stopped = true;
659
+ this.authGeneration += 1;
660
+ this.authClassification = "unknown";
661
+ this.authResolution = Promise.resolve();
662
+ this.authResolutionPending = false;
551
663
  this.pendingInputQueue = [];
552
664
  if (this.queryInstance) {
553
665
  console.log("[sdk-process] Stopping query");
@@ -558,6 +670,7 @@ export class SdkProcess extends EventEmitter {
558
670
  this.userMessageResolve = null;
559
671
  this.toolCallsSinceLastResult = 0;
560
672
  this.fileEditsSinceLastResult = 0;
673
+ this.pendingAssistantError = null;
561
674
  // Emit session_end so listeners can re-persist metadata before cleanup.
562
675
  // processMessages() won't reach its session_end emit because close()
563
676
  // causes the iterator to throw and the error is suppressed.
@@ -596,15 +709,7 @@ export class SdkProcess extends EventEmitter {
596
709
  }
597
710
  const resolve = this.userMessageResolve;
598
711
  this.userMessageResolve = null;
599
- resolve({
600
- type: "user",
601
- session_id: this._sessionId ?? "",
602
- message: {
603
- role: "user",
604
- content: [{ type: "text", text }],
605
- },
606
- parent_tool_use_id: null,
607
- });
712
+ this.resolveUserMessage(resolve, text);
608
713
  return { queued: false, shouldInterrupt: false };
609
714
  }
610
715
  sendInput(text) {
@@ -625,31 +730,9 @@ export class SdkProcess extends EventEmitter {
625
730
  }
626
731
  const resolve = this.userMessageResolve;
627
732
  this.userMessageResolve = null;
628
- const content = [];
629
- // Add image blocks first (Claude processes images before text)
630
- for (const image of images) {
631
- content.push({
632
- type: "image",
633
- source: {
634
- type: "base64",
635
- media_type: image.mimeType,
636
- data: image.base64,
637
- },
638
- });
639
- }
640
- // Add text block
641
- content.push({ type: "text", text });
642
733
  const totalKB = images.reduce((sum, img) => sum + Math.round(img.base64.length / 1024), 0);
643
734
  console.log(`[sdk-process] Sending message with ${images.length} image(s) (${totalKB}KB base64 total)`);
644
- resolve({
645
- type: "user",
646
- session_id: this._sessionId ?? "",
647
- message: {
648
- role: "user",
649
- content,
650
- },
651
- parent_tool_use_id: null,
652
- });
735
+ this.resolveUserMessage(resolve, text, images);
653
736
  return { queued: false, shouldInterrupt: false };
654
737
  }
655
738
  sendInputWithImages(text, images) {
@@ -886,33 +969,16 @@ export class SdkProcess extends EventEmitter {
886
969
  }
887
970
  async *createUserMessageStream() {
888
971
  while (!this.stopped) {
889
- // Drain queued messages first (FIFO order)
890
- if (this.pendingInputQueue.length > 0) {
972
+ // A queued mid-turn input must wait for result so it cannot overtake
973
+ // the interrupted turn. Once idle, each consumer request drains FIFO.
974
+ const turnInProgress = this._status === "running" ||
975
+ this._status === "compacting" ||
976
+ this._status === "waiting_approval";
977
+ if (this.pendingInputQueue.length > 0 && !turnInProgress) {
891
978
  const { text, images } = this.pendingInputQueue.shift();
892
979
  console.log(`[sdk-process] Sending queued input${images ? ` with ${images.length} image(s)` : ""} (remaining: ${this.pendingInputQueue.length})`);
893
- const content = [];
894
- if (images) {
895
- for (const image of images) {
896
- content.push({
897
- type: "image",
898
- source: {
899
- type: "base64",
900
- media_type: image.mimeType,
901
- data: image.base64,
902
- },
903
- });
904
- }
905
- }
906
- content.push({ type: "text", text });
907
- yield {
908
- type: "user",
909
- session_id: this._sessionId ?? "",
910
- message: {
911
- role: "user",
912
- content,
913
- },
914
- parent_tool_use_id: null,
915
- };
980
+ this.setStatus("running");
981
+ yield this.buildUserMessage(text, images);
916
982
  continue;
917
983
  }
918
984
  const msg = await new Promise((resolve) => {
@@ -923,30 +989,99 @@ export class SdkProcess extends EventEmitter {
923
989
  yield msg;
924
990
  }
925
991
  }
926
- async processMessages() {
992
+ async resolveAuthClassification(queryInstance, generation) {
993
+ if (!queryInstance)
994
+ return;
995
+ const initializationResult = queryInstance.initializationResult;
996
+ if (typeof initializationResult !== "function")
997
+ return;
998
+ this.authResolutionPending = true;
999
+ const initializationPromise = Promise.resolve().then(() => initializationResult.call(queryInstance));
1000
+ const settlement = (async () => {
1001
+ try {
1002
+ const initialization = await initializationPromise;
1003
+ if (generation !== this.authGeneration)
1004
+ return;
1005
+ this.authResolutionPending = false;
1006
+ this.applyAuthSource(initialization.account?.apiKeySource, generation);
1007
+ }
1008
+ catch (error) {
1009
+ if (generation !== this.authGeneration)
1010
+ return;
1011
+ this.authResolutionPending = false;
1012
+ console.warn(`[sdk-process] Could not resolve Claude auth source: ${error instanceof Error ? error.message : String(error)}`);
1013
+ }
1014
+ })();
1015
+ let timeoutId;
1016
+ try {
1017
+ const timeout = new Promise((resolve) => {
1018
+ timeoutId = setTimeout(() => resolve(false), CLAUDE_AUTH_RESOLUTION_TIMEOUT_MS);
1019
+ });
1020
+ const settledBeforeTimeout = await Promise.race([
1021
+ settlement.then(() => true),
1022
+ timeout,
1023
+ ]);
1024
+ if (!settledBeforeTimeout && generation === this.authGeneration) {
1025
+ console.warn("[sdk-process] Timed out resolving Claude auth source");
1026
+ }
1027
+ }
1028
+ finally {
1029
+ if (timeoutId)
1030
+ clearTimeout(timeoutId);
1031
+ }
1032
+ }
1033
+ applyAuthSource(source, generation) {
1034
+ if (generation !== this.authGeneration)
1035
+ return true;
1036
+ if (source === "oauth") {
1037
+ this.authClassification = "subscription";
1038
+ }
1039
+ else if (typeof source === "string" &&
1040
+ this.authClassification === "unknown") {
1041
+ this.authClassification = "api_key";
1042
+ }
1043
+ if (this.authClassification === "subscription" &&
1044
+ !isClaudeOAuthOptInEnabled()) {
1045
+ console.log("[sdk-process] OAuth auth source requires explicit opt-in");
1046
+ this.emitMessage({
1047
+ type: "error",
1048
+ message: CLAUDE_OAUTH_OPT_IN_MESSAGE,
1049
+ errorCode: CLAUDE_OAUTH_OPT_IN_ERROR_CODE,
1050
+ });
1051
+ this.stop();
1052
+ this.emit("exit", 1);
1053
+ return false;
1054
+ }
1055
+ return true;
1056
+ }
1057
+ async processMessages(authGeneration, authResolution) {
927
1058
  if (!this.queryInstance)
928
1059
  return;
929
1060
  for await (const message of this.queryInstance) {
930
- if (this.stopped)
1061
+ if (this.stopped || authGeneration !== this.authGeneration)
931
1062
  break;
932
1063
  if (message.type === "system" &&
933
1064
  "subtype" in message &&
934
1065
  message.subtype === "init" &&
935
- message.apiKeySource === "oauth" &&
936
- !isClaudeOAuthOptInEnabled()) {
937
- console.log("[sdk-process] OAuth auth source requires explicit opt-in");
938
- this.emitMessage({
939
- type: "error",
940
- message: CLAUDE_OAUTH_OPT_IN_MESSAGE,
941
- errorCode: CLAUDE_OAUTH_OPT_IN_ERROR_CODE,
942
- });
943
- this.stop();
944
- this.emit("exit", 1);
1066
+ !this.applyAuthSource(message.apiKeySource, authGeneration)) {
945
1067
  return;
946
1068
  }
947
1069
  // Convert SDK message to ServerMessage
948
1070
  let serverMsg = sdkMessageToServerMessage(message);
1071
+ if (message.type === "assistant" && this.pendingAssistantError === null) {
1072
+ this.pendingAssistantError = claudeAssistantErrorMessage(message.error);
1073
+ }
949
1074
  if (serverMsg?.type === "result") {
1075
+ if (this.authClassification !== "subscription") {
1076
+ await authResolution;
1077
+ }
1078
+ if (this.stopped || authGeneration !== this.authGeneration)
1079
+ return;
1080
+ if (this.authClassification !== "api_key" ||
1081
+ this.authResolutionPending) {
1082
+ const { cost: _estimatedApiCost, ...withoutCost } = serverMsg;
1083
+ serverMsg = withoutCost;
1084
+ }
950
1085
  if (this.toolCallsSinceLastResult > 0 || this.fileEditsSinceLastResult > 0) {
951
1086
  serverMsg = {
952
1087
  ...serverMsg,
@@ -961,6 +1096,23 @@ export class SdkProcess extends EventEmitter {
961
1096
  this.toolCallsSinceLastResult = 0;
962
1097
  this.fileEditsSinceLastResult = 0;
963
1098
  }
1099
+ if (message.type === "result" && this.pendingAssistantError) {
1100
+ const result = message;
1101
+ const hasRealResultError = serverMsg?.type === "result" &&
1102
+ serverMsg.subtype === "error" &&
1103
+ Array.isArray(result.errors) &&
1104
+ result.errors.some((error) => typeof error === "string" &&
1105
+ !isInternalClaudeResultError(error));
1106
+ if (!hasRealResultError) {
1107
+ this.flushPendingAssistantError();
1108
+ if (serverMsg?.type === "result" && serverMsg.subtype === "error") {
1109
+ serverMsg = null;
1110
+ }
1111
+ }
1112
+ else {
1113
+ this.pendingAssistantError = null;
1114
+ }
1115
+ }
964
1116
  if (serverMsg) {
965
1117
  this.emitMessage(serverMsg);
966
1118
  }
@@ -1000,7 +1152,17 @@ export class SdkProcess extends EventEmitter {
1000
1152
  // Update status from message type
1001
1153
  this.updateStatusFromMessage(message);
1002
1154
  }
1155
+ await authResolution;
1156
+ if (this.stopped || authGeneration !== this.authGeneration)
1157
+ return;
1158
+ this.flushPendingAssistantError();
1003
1159
  // Query finished — CLI has completed shutdown including file writes.
1160
+ // Treat natural completion as the end of this auth generation so an
1161
+ // initializationResult that settles after the timeout cannot emit a
1162
+ // second, contradictory terminal event for an already-finished query.
1163
+ this.authGeneration += 1;
1164
+ this.authResolutionPending = false;
1165
+ this.authResolution = Promise.resolve();
1004
1166
  this.queryInstance = null;
1005
1167
  // Emit session_end before exit so listeners can re-persist metadata
1006
1168
  // (e.g. customTitle) that the CLI may have overwritten during shutdown.
@@ -1008,6 +1170,14 @@ export class SdkProcess extends EventEmitter {
1008
1170
  this.setStatus("idle");
1009
1171
  this.emit("exit", 0);
1010
1172
  }
1173
+ flushPendingAssistantError() {
1174
+ if (!this.pendingAssistantError)
1175
+ return false;
1176
+ const message = this.pendingAssistantError;
1177
+ this.pendingAssistantError = null;
1178
+ this.emitMessage(message);
1179
+ return true;
1180
+ }
1011
1181
  /**
1012
1182
  * Core permission handler: called by SDK before each tool execution.
1013
1183
  * Returns a Promise that resolves when the user approves/rejects.
@@ -1053,9 +1223,21 @@ export class SdkProcess extends EventEmitter {
1053
1223
  }
1054
1224
  updateStatusFromMessage(msg) {
1055
1225
  switch (msg.type) {
1056
- case "system":
1057
- // Already handled in processMessages for init
1226
+ case "system": {
1227
+ const system = msg;
1228
+ if (system.subtype === "status") {
1229
+ if (system.status === "compacting") {
1230
+ this.setStatus("compacting");
1231
+ }
1232
+ else if (system.status === "requesting") {
1233
+ this.setStatus("running");
1234
+ }
1235
+ else if (system.status === null && this._status === "compacting") {
1236
+ this.setStatus("running");
1237
+ }
1238
+ }
1058
1239
  break;
1240
+ }
1059
1241
  case "assistant":
1060
1242
  if (this.pendingPermissions.size === 0) {
1061
1243
  this.setStatus("running");
@@ -1069,9 +1251,45 @@ export class SdkProcess extends EventEmitter {
1069
1251
  case "result":
1070
1252
  this.pendingPermissions.clear();
1071
1253
  this.setStatus("idle");
1254
+ this.deliverQueuedInputIfWaiting();
1072
1255
  break;
1073
1256
  }
1074
1257
  }
1258
+ buildUserMessage(text, images) {
1259
+ const content = [];
1260
+ if (images) {
1261
+ for (const image of images) {
1262
+ content.push({
1263
+ type: "image",
1264
+ source: {
1265
+ type: "base64",
1266
+ media_type: image.mimeType,
1267
+ data: image.base64,
1268
+ },
1269
+ });
1270
+ }
1271
+ }
1272
+ content.push({ type: "text", text });
1273
+ return {
1274
+ type: "user",
1275
+ session_id: this._sessionId ?? "",
1276
+ message: { role: "user", content },
1277
+ parent_tool_use_id: null,
1278
+ };
1279
+ }
1280
+ deliverQueuedInputIfWaiting() {
1281
+ const resolve = this.userMessageResolve;
1282
+ const queued = this.pendingInputQueue[0];
1283
+ if (!resolve || !queued)
1284
+ return;
1285
+ this.userMessageResolve = null;
1286
+ this.pendingInputQueue.shift();
1287
+ this.resolveUserMessage(resolve, queued.text, queued.images);
1288
+ }
1289
+ resolveUserMessage(resolve, text, images) {
1290
+ this.setStatus("running");
1291
+ resolve(this.buildUserMessage(text, images));
1292
+ }
1075
1293
  handlePostToolUseHook(input) {
1076
1294
  if (!input || typeof input !== "object" || Array.isArray(input)) {
1077
1295
  return;