@1presence/bridge 0.34.0 → 0.35.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 +59 -18
- package/dist/index.js +8 -0
- package/package.json +1 -1
package/dist/claude.js
CHANGED
|
@@ -111,15 +111,32 @@ function renderHistoryMessage(msg) {
|
|
|
111
111
|
}
|
|
112
112
|
// ─── Active processes ─────────────────────────────────────────────────────────
|
|
113
113
|
const active = new Map();
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
114
|
+
// conversationId → pending retry timer. A retry is scheduled with a backoff
|
|
115
|
+
// delay, during which the conversation has NO entry in `active`. If a new user
|
|
116
|
+
// message arrives in that window it must cancel the stale retry (otherwise the
|
|
117
|
+
// retry would re-run the OLD turn's history and clobber the new one). The
|
|
118
|
+
// supersede block clears any pending timer here before spawning.
|
|
119
|
+
const pendingRetries = new Map();
|
|
120
|
+
// Automatic retries when the `claude` CLI exits non-zero BEFORE producing any
|
|
121
|
+
// real output. This covers the known Claude Code print-mode 400 regression that
|
|
122
|
+
// surfaces as "API Error: 400 due to tool use concurrency issues" (GitHub
|
|
123
|
+
// anthropics/claude-code#18131, still open) — it is non-deterministic enough
|
|
124
|
+
// that a fresh spawn often succeeds. We retry ONLY when the failed attempt
|
|
125
|
+
// produced no real assistant text and no tool calls, so a failure that lands
|
|
126
|
+
// after real work (where retrying could double-execute a side-effectful tool)
|
|
127
|
+
// is surfaced, never silently re-run.
|
|
128
|
+
//
|
|
129
|
+
// 2 retries = up to 3 attempts/turn. The first retry captures nearly all of the
|
|
130
|
+
// transient wins; further attempts buy little on transient failures but add
|
|
131
|
+
// latency and re-send the full (1M-context) history again on deterministic ones
|
|
132
|
+
// — see vault Bugs.md. Retries use escalating backoff (avoids a subscription
|
|
133
|
+
// rate-limit cascade from rapid re-spawns) and stop once total retry time
|
|
134
|
+
// exceeds the wall-clock cap, so a slow-failing attempt can't strand the user.
|
|
135
|
+
// All below the SSE boundary, so the user sees only a slightly longer
|
|
136
|
+
// "thinking" gap, never an intermediate error.
|
|
137
|
+
const MAX_TURN_RETRIES = 2;
|
|
138
|
+
const RETRY_BACKOFF_BASE_MS = 750; // delay = base * attempt# → 750ms, 1500ms
|
|
139
|
+
const RETRY_WALL_CLOCK_CAP_MS = 12_000; // stop retrying past this much elapsed
|
|
123
140
|
// Map a non-zero CLI exit + any captured "API Error:" line to a concise,
|
|
124
141
|
// user-facing Local Mode message. The raw upstream text stays in operator logs
|
|
125
142
|
// only — we never echo a wall of provider error JSON into the chat. Referring
|
|
@@ -135,7 +152,7 @@ const MAX_TURN_RETRIES = 1;
|
|
|
135
152
|
function describeCliFailure(code, apiErrorText) {
|
|
136
153
|
const t = apiErrorText.trim();
|
|
137
154
|
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
|
|
155
|
+
return 'Local Mode hit a known Claude Code error (a print-mode bug that affects every current version). I retried a few times automatically — sending the message again sometimes gets through. See https://github.com/anthropics/claude-code/issues/18131';
|
|
139
156
|
}
|
|
140
157
|
if (/^API Error:/i.test(t)) {
|
|
141
158
|
return `Local Mode error from Claude Code: ${t.replace(/^API Error:\s*/i, '').trim()}`;
|
|
@@ -144,8 +161,9 @@ function describeCliFailure(code, apiErrorText) {
|
|
|
144
161
|
}
|
|
145
162
|
// ─── Spawn ────────────────────────────────────────────────────────────────────
|
|
146
163
|
function spawnClaude(params) {
|
|
147
|
-
const { conversationId, presenceSessionId, text, uid, history, vaultFileOpen, clientCapabilities, syncedFolders, onEvent, onDone, onError } = params;
|
|
164
|
+
const { conversationId, presenceSessionId, text, uid, history, vaultFileOpen, clientCapabilities, syncedFolders, onEvent, onDone, onError, onNotice } = params;
|
|
148
165
|
const attemptIdx = params._attemptIdx ?? 0;
|
|
166
|
+
const firstAttemptAt = params._firstAttemptAt ?? Date.now();
|
|
149
167
|
const systemPromptPath = (0, path_1.join)((0, os_1.tmpdir)(), `agent-${uid}.md`);
|
|
150
168
|
const mcpConfigPath = (0, path_1.join)((0, os_1.tmpdir)(), `mcp-${uid}.json`);
|
|
151
169
|
if (verbose) {
|
|
@@ -201,6 +219,19 @@ function spawnClaude(params) {
|
|
|
201
219
|
existing.kill('SIGTERM');
|
|
202
220
|
active.delete(conversationId);
|
|
203
221
|
}
|
|
222
|
+
// Cancel any retry scheduled for this conversation that hasn't fired yet.
|
|
223
|
+
// Without this, a new user message arriving during a retry's backoff window
|
|
224
|
+
// would race the stale retry — which carries the OLD turn's history and would
|
|
225
|
+
// clobber the new turn. Skip when this call IS the retry firing (attemptIdx>0,
|
|
226
|
+
// the timer already deleted itself before invoking us).
|
|
227
|
+
if (attemptIdx === 0) {
|
|
228
|
+
const pending = pendingRetries.get(conversationId);
|
|
229
|
+
if (pending) {
|
|
230
|
+
clearTimeout(pending);
|
|
231
|
+
pendingRetries.delete(conversationId);
|
|
232
|
+
process.stderr.write(`[bridge] cancelled pending retry for ${conversationId} (superseded by new turn)\n`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
204
235
|
// Note: ephemeral context (vault_file_open / client_capabilities / synced_folders)
|
|
205
236
|
// is injected into the last user message by the gateway BEFORE history is
|
|
206
237
|
// sent over the WS. The bridge no longer constructs `userMessageText` —
|
|
@@ -506,13 +537,23 @@ function spawnClaude(params) {
|
|
|
506
537
|
catch { /* ignore */ }
|
|
507
538
|
}
|
|
508
539
|
if (code !== 0 && code !== null) {
|
|
509
|
-
// Auto-retry
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
540
|
+
// Auto-retry when the CLI failed BEFORE producing any real output — the
|
|
541
|
+
// signature of the known print-mode 400 regression. A fresh spawn (new
|
|
542
|
+
// --session-id) often succeeds. We never retry once real text or a tool
|
|
543
|
+
// call landed, to avoid double-running a side-effectful tool. Retries use
|
|
544
|
+
// escalating backoff and stop past the wall-clock cap (see consts above).
|
|
545
|
+
const elapsed = Date.now() - firstAttemptAt;
|
|
546
|
+
if (attemptIdx < MAX_TURN_RETRIES && sawApiError && !producedRealOutput && elapsed < RETRY_WALL_CLOCK_CAP_MS) {
|
|
547
|
+
const delay = RETRY_BACKOFF_BASE_MS * (attemptIdx + 1);
|
|
548
|
+
const nextAttempt = attemptIdx + 2;
|
|
549
|
+
process.stderr.write(`[bridge] turn failed before output (${apiErrorText.replace(/\n+/g, ' ').slice(0, 120)}) — retrying (${nextAttempt} of ${MAX_TURN_RETRIES + 1}) in ${delay}ms\n`);
|
|
550
|
+
// Admin-only ephemeral thread notice — jargon is fine in Local Mode.
|
|
551
|
+
onNotice?.(`Claude Code print-mode 400 (tool-use concurrency, anthropics/claude-code#18131) — respawning, attempt ${nextAttempt}/${MAX_TURN_RETRIES + 1}…`);
|
|
552
|
+
const timer = setTimeout(() => {
|
|
553
|
+
pendingRetries.delete(conversationId);
|
|
554
|
+
spawnClaude({ ...params, _attemptIdx: attemptIdx + 1, _firstAttemptAt: firstAttemptAt });
|
|
555
|
+
}, delay);
|
|
556
|
+
pendingRetries.set(conversationId, timer);
|
|
516
557
|
return;
|
|
517
558
|
}
|
|
518
559
|
// Pass any partial token usage we observed before the failure so the
|
package/dist/index.js
CHANGED
|
@@ -244,6 +244,14 @@ async function handleMessage(conversationId, text, sessionId, history, auth, vau
|
|
|
244
244
|
currentWs.send(JSON.stringify({ type: 'stream', conversationId, event }));
|
|
245
245
|
}
|
|
246
246
|
},
|
|
247
|
+
onNotice: (message) => {
|
|
248
|
+
// Ephemeral, non-persisted thread notice (admin-only Local Mode). Relayed
|
|
249
|
+
// by the gateway to the PWA SSE stream as a `notice` AgentEvent; it does
|
|
250
|
+
// NOT go through the turn accumulator, so it never lands in history.
|
|
251
|
+
if (currentWs?.readyState === ws_1.default.OPEN) {
|
|
252
|
+
currentWs.send(JSON.stringify({ type: 'notice', conversationId, message }));
|
|
253
|
+
}
|
|
254
|
+
},
|
|
247
255
|
onDone: (messageCount, costUsd, usage, model) => {
|
|
248
256
|
const parts = [];
|
|
249
257
|
if (usage)
|