@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.
- package/dist/claude.js +91 -9
- 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',
|
|
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 (
|
|
374
|
-
//
|
|
375
|
-
|
|
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
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|