@1presence/bridge 0.33.0 → 0.34.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 (2) hide show
  1. package/dist/claude.js +91 -9
  2. package/package.json +1 -1
package/dist/claude.js CHANGED
@@ -111,9 +111,41 @@ function renderHistoryMessage(msg) {
111
111
  }
112
112
  // ─── Active processes ─────────────────────────────────────────────────────────
113
113
  const active = new Map();
114
+ // Maximum automatic retries when the `claude` CLI exits non-zero BEFORE
115
+ // producing any real output. This covers the known Claude Code print-mode 400
116
+ // regression that surfaces as "API Error: 400 due to tool use concurrency
117
+ // issues" (GitHub anthropics/claude-code#18131, still open) — it is
118
+ // non-deterministic enough that a fresh spawn frequently succeeds. We retry
119
+ // ONLY when the failed attempt produced no real assistant text and no tool
120
+ // calls, so a failure that lands after real work (where retrying could
121
+ // double-execute a side-effectful tool) is surfaced, never silently re-run.
122
+ const MAX_TURN_RETRIES = 1;
123
+ // Map a non-zero CLI exit + any captured "API Error:" line to a concise,
124
+ // user-facing Local Mode message. The raw upstream text stays in operator logs
125
+ // only — we never echo a wall of provider error JSON into the chat. Referring
126
+ // to "Claude Code" here is intentional and consistent with Local Mode's other
127
+ // operational errors (e.g. the "claude CLI not found" message): in Local Mode
128
+ // the user is knowingly running their own Claude Code install.
129
+ //
130
+ // NOTE on the 400 tool-use case: this is an open Claude Code print-mode
131
+ // regression (introduced in 2.1.19, still present in 2.1.146 — the current
132
+ // latest), so upgrading does NOT fix it. We deliberately do not suggest an
133
+ // upgrade; the automatic retry is the real mitigation and resending sometimes
134
+ // gets through.
135
+ function describeCliFailure(code, apiErrorText) {
136
+ const t = apiErrorText.trim();
137
+ if (/API Error:\s*400/i.test(t) && /(tool use|concurren|parallel)/i.test(t)) {
138
+ return 'Local Mode hit a known Claude Code error (a print-mode bug that affects every current version). I retried once automatically — sending the message again sometimes gets through.';
139
+ }
140
+ if (/^API Error:/i.test(t)) {
141
+ return `Local Mode error from Claude Code: ${t.replace(/^API Error:\s*/i, '').trim()}`;
142
+ }
143
+ return `Local Mode stopped unexpectedly (claude exited with code ${code ?? 'unknown'}). Please try again.`;
144
+ }
114
145
  // ─── Spawn ────────────────────────────────────────────────────────────────────
115
146
  function spawnClaude(params) {
116
147
  const { conversationId, presenceSessionId, text, uid, history, vaultFileOpen, clientCapabilities, syncedFolders, onEvent, onDone, onError } = params;
148
+ const attemptIdx = params._attemptIdx ?? 0;
117
149
  const systemPromptPath = (0, path_1.join)((0, os_1.tmpdir)(), `agent-${uid}.md`);
118
150
  const mcpConfigPath = (0, path_1.join)((0, os_1.tmpdir)(), `mcp-${uid}.json`);
119
151
  if (verbose) {
@@ -125,6 +157,13 @@ function spawnClaude(params) {
125
157
  process.stderr.write(paint('90', `[bridge:verbose] conversation: ${conversationId}`) + '\n');
126
158
  process.stderr.write(paint('90', `[bridge:verbose] history turns: ${history.length}`) + '\n');
127
159
  }
160
+ // Surface the user's UID before the session line in every mode — it's the
161
+ // Firestore doc prefix (`sessions/<uid>_<conversationId>`), so logging it
162
+ // makes a reported bridge failure correlatable to the stored session without
163
+ // having to ask which account hit it. The CLI's own `--session-id` is
164
+ // ephemeral and is NOT the Firestore conversationId, so the uid is the key
165
+ // join column when debugging.
166
+ process.stderr.write(`[bridge] user ${uid}\n`);
128
167
  // Debug transcript: lead with the user prompt for this turn (the clean
129
168
  // message, before the gateway's ephemeral-context prefix), plus the session
130
169
  // id (correlates with the chat URL / Firestore session doc) and a hint at
@@ -198,6 +237,10 @@ function spawnClaude(params) {
198
237
  // across turns of a chat — even with --no-session-persistence. The
199
238
  // bridge passes the per-spawn `conversationId` here; the presence
200
239
  // sessionId is correlated separately via bridge logs and spool records.
240
+ // The CLI treats --session-id as "claim this new session ID" and rejects a
241
+ // reused id with "Session ID X is already in use". A retry is a fresh spawn,
242
+ // so it MUST use a new uuid; the first attempt keeps the correlation id.
243
+ const spawnSessionId = attemptIdx === 0 ? presenceSessionId : crypto.randomUUID();
201
244
  const args = [
202
245
  '--print',
203
246
  '--input-format', 'stream-json',
@@ -210,7 +253,7 @@ function spawnClaude(params) {
210
253
  '--mcp-config', mcpConfigPath,
211
254
  '--strict-mcp-config',
212
255
  '--no-session-persistence',
213
- '--session-id', presenceSessionId,
256
+ '--session-id', spawnSessionId,
214
257
  ];
215
258
  const pinnedModel = (0, config_1.getBridgeModel)();
216
259
  if (pinnedModel) {
@@ -266,6 +309,15 @@ function spawnClaude(params) {
266
309
  let extractedModel = null;
267
310
  let buffer = '';
268
311
  let killedForViolation = false;
312
+ // Retry/error-surfacing tracking for this attempt:
313
+ // - sawApiError: the CLI emitted an "API Error:" assistant text event (the
314
+ // way Claude Code reports an underlying API failure mid-turn).
315
+ // - apiErrorText: that text, captured for describeCliFailure().
316
+ // - producedRealOutput: any real assistant text or tool_use was emitted, so
317
+ // a later failure must NOT be retried (could double-run a side-effect).
318
+ let sawApiError = false;
319
+ let apiErrorText = '';
320
+ let producedRealOutput = false;
269
321
  proc.stdout.on('data', (chunk) => {
270
322
  buffer += chunk.toString('utf-8');
271
323
  const lines = buffer.split('\n');
@@ -282,6 +334,10 @@ function spawnClaude(params) {
282
334
  continue;
283
335
  }
284
336
  const type = event['type'];
337
+ // Set when this event is the CLI's "API Error:" turn — we neither forward
338
+ // it to the PWA nor let it reach the accumulator (it carries no real
339
+ // content and would poison history / show a raw error mid-stream).
340
+ let suppressEvent = false;
285
341
  // Extract model + key source info from the first system/init event.
286
342
  // No session-id persistence — Firestore is the only source of truth
287
343
  // now, and we pin --session-id to presenceSessionId on every spawn.
@@ -322,6 +378,7 @@ function spawnClaude(params) {
322
378
  let wroteText = false;
323
379
  for (const block of content) {
324
380
  if (block['type'] === 'tool_use') {
381
+ producedRealOutput = true;
325
382
  const toolName = block['name'];
326
383
  const toolId = block['id'];
327
384
  if (toolId)
@@ -370,13 +427,27 @@ function spawnClaude(params) {
370
427
  else if (block['type'] === 'text') {
371
428
  const text = block['text'];
372
429
  if (text) {
373
- if (debug) {
374
- // Full text, newlines intact the readable transcript.
375
- debugBlock('assistant', exports.SECTION_COLORS.assistant, text);
430
+ if (/^API Error:/i.test(text.trimStart())) {
431
+ // The CLI is reporting an underlying API failure as assistant
432
+ // text. Capture it for the user-facing message, and suppress
433
+ // the whole event so the raw error never reaches the PWA or
434
+ // the accumulator (the gateway also blanks it via
435
+ // cleanTurnText — this is the upstream defense).
436
+ sawApiError = true;
437
+ apiErrorText = text.trim();
438
+ suppressEvent = true;
439
+ process.stderr.write(paint(exports.SECTION_COLORS.result, `[bridge] ${text.replace(/\n+/g, ' ')}`) + '\n');
376
440
  }
377
441
  else {
378
- process.stderr.write(paint(exports.SECTION_COLORS.assistant, text.replace(/\n+/g, ' ')));
379
- wroteText = true;
442
+ producedRealOutput = true;
443
+ if (debug) {
444
+ // Full text, newlines intact — the readable transcript.
445
+ debugBlock('assistant', exports.SECTION_COLORS.assistant, text);
446
+ }
447
+ else {
448
+ process.stderr.write(paint(exports.SECTION_COLORS.assistant, text.replace(/\n+/g, ' ')));
449
+ wroteText = true;
450
+ }
380
451
  }
381
452
  }
382
453
  }
@@ -412,7 +483,8 @@ function spawnClaude(params) {
412
483
  if (typeof c === 'number')
413
484
  costUsd = c;
414
485
  }
415
- onEvent(event);
486
+ if (!suppressEvent)
487
+ onEvent(event);
416
488
  }
417
489
  });
418
490
  proc.stderr.on('data', (chunk) => {
@@ -434,9 +506,19 @@ function spawnClaude(params) {
434
506
  catch { /* ignore */ }
435
507
  }
436
508
  if (code !== 0 && code !== null) {
509
+ // Auto-retry once when the CLI failed BEFORE producing any real output —
510
+ // the signature of the known print-mode 400 regression. A fresh spawn
511
+ // (new --session-id) usually succeeds. We never retry once real text or a
512
+ // tool call landed, to avoid double-running a side-effectful tool.
513
+ if (attemptIdx < MAX_TURN_RETRIES && sawApiError && !producedRealOutput) {
514
+ process.stderr.write(`[bridge] turn failed before output (${apiErrorText.replace(/\n+/g, ' ').slice(0, 120)}) — retrying once\n`);
515
+ spawnClaude({ ...params, _attemptIdx: attemptIdx + 1 });
516
+ return;
517
+ }
437
518
  // Pass any partial token usage we observed before the failure so the
438
- // PWA and the gateway's bridge usage store can still record it.
439
- onError(`claude exited with code ${code}`, usage, extractedModel);
519
+ // PWA and the gateway's bridge usage store can still record it. Surface a
520
+ // classified, user-readable message instead of the opaque exit code.
521
+ onError(describeCliFailure(code, apiErrorText), usage, extractedModel);
440
522
  }
441
523
  else {
442
524
  onDone(messageCount, costUsd, usage, extractedModel);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "Run 1Presence on your Mac and use your Claude.ai Pro subscription from any device",
5
5
  "bin": {
6
6
  "1presence-bridge": "dist/index.js"