@1presence/bridge 0.60.0 → 0.61.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 +63 -1
- package/package.json +1 -1
package/dist/claude.js
CHANGED
|
@@ -195,6 +195,35 @@ function joinErrorDetail(...parts) {
|
|
|
195
195
|
// Lines the SDK/CLI writes to its own stderr that look like a failure — surfaced
|
|
196
196
|
// even outside verbose mode so a turn that dies upstream leaves a trail.
|
|
197
197
|
const SDK_STDERR_ERROR_RE = /\b(error|exception|fail(?:ed|ure)?|invalid|unauthor|forbidden|refus|denied|40[0-9]|429|5\d\d|overloaded|rate.?limit)\b/i;
|
|
198
|
+
// The remote 1Presence MCP server's key (matches index.ts writeMcpConfig +
|
|
199
|
+
// the `mcp__1presence__*` allowlist). reconnectMcpServer() takes this name.
|
|
200
|
+
const REMOTE_MCP_SERVER_NAME = '1presence';
|
|
201
|
+
// Signature of a dropped remote MCP session. The pod binds each MCP session to a
|
|
202
|
+
// single long-lived SSE `GET /mcp` stream held in its memory; if that stream
|
|
203
|
+
// drops mid-run (transient network blip on a long workflow stage), the pod
|
|
204
|
+
// deletes the session and every later `POST /mcp/message?sessionId=…` returns
|
|
205
|
+
// HTTP 404 "session not found or expired". The SDK does NOT auto-retry an
|
|
206
|
+
// expired MCP session (sdk.d.ts: "Session expiry is not retried automatically;
|
|
207
|
+
// callers can mcp_reconnect and retry") — it surfaces the 404 as a tool_result
|
|
208
|
+
// error to the model, which then dead-loops calling tools against the same dead
|
|
209
|
+
// session for the rest of the turn. Detecting this lets us reconnect the server
|
|
210
|
+
// so the model's next attempt re-handshakes a fresh session and recovers.
|
|
211
|
+
const MCP_SESSION_EXPIRED_RE = /session not found or expired|Error POSTing to endpoint \(HTTP 404\)/i;
|
|
212
|
+
// Pull every text fragment out of a tool_result `content` (string OR the MCP
|
|
213
|
+
// block-array shape `[{type:'text',text:'…'}]`) so we can scan it for the
|
|
214
|
+
// session-expiry signature above.
|
|
215
|
+
function toolResultText(content) {
|
|
216
|
+
if (typeof content === 'string')
|
|
217
|
+
return content;
|
|
218
|
+
if (Array.isArray(content)) {
|
|
219
|
+
return content
|
|
220
|
+
.map((b) => (b && typeof b === 'object' && typeof b.text === 'string'
|
|
221
|
+
? b.text
|
|
222
|
+
: ''))
|
|
223
|
+
.join(' ');
|
|
224
|
+
}
|
|
225
|
+
return '';
|
|
226
|
+
}
|
|
198
227
|
/**
|
|
199
228
|
* Copy for an actionable rate-limit notice. The SDK emits `rate_limit_event`
|
|
200
229
|
* whenever rate-limit info CHANGES — including the routine `allowed` case on
|
|
@@ -649,8 +678,28 @@ export function spawnClaude(params) {
|
|
|
649
678
|
...(pinnedModel ? { model: pinnedModel } : {}),
|
|
650
679
|
};
|
|
651
680
|
const promptMessages = buildPromptMessages(history);
|
|
681
|
+
// Throttle remote-MCP reconnects: a single dropped session produces a burst
|
|
682
|
+
// of 404 tool_results (the model keeps trying), so reconnect at most once per
|
|
683
|
+
// window to avoid a reconnect storm. A reconnect is in-flight guard too.
|
|
684
|
+
let lastMcpReconnectAt = 0;
|
|
685
|
+
let mcpReconnecting = false;
|
|
686
|
+
const MCP_RECONNECT_THROTTLE_MS = 10_000;
|
|
687
|
+
const maybeReconnectRemoteMcp = (q) => {
|
|
688
|
+
const now = Date.now();
|
|
689
|
+
if (mcpReconnecting || now - lastMcpReconnectAt < MCP_RECONNECT_THROTTLE_MS)
|
|
690
|
+
return;
|
|
691
|
+
mcpReconnecting = true;
|
|
692
|
+
lastMcpReconnectAt = now;
|
|
693
|
+
process.stderr.write(paint(SECTION_COLORS.result, '[bridge] remote MCP session expired — reconnecting and retrying') + '\n');
|
|
694
|
+
onNotice?.('Reconnecting to 1Presence tools…');
|
|
695
|
+
void q.reconnectMcpServer(REMOTE_MCP_SERVER_NAME)
|
|
696
|
+
.then(() => process.stderr.write(paint(SECTION_COLORS.result, '[bridge] remote MCP reconnected') + '\n'))
|
|
697
|
+
.catch((err) => process.stderr.write(paint(SECTION_COLORS.result, `[bridge] remote MCP reconnect failed: ${err.message}`) + '\n'))
|
|
698
|
+
.finally(() => { mcpReconnecting = false; });
|
|
699
|
+
};
|
|
652
700
|
try {
|
|
653
|
-
|
|
701
|
+
const q = query({ prompt: promptStream(promptMessages), options });
|
|
702
|
+
for await (const m of q) {
|
|
654
703
|
// Skip echoed input replays — they would double-count in the accumulator
|
|
655
704
|
// and re-stream prior turns to the PWA.
|
|
656
705
|
if (m.isReplay)
|
|
@@ -716,6 +765,19 @@ export function spawnClaude(params) {
|
|
|
716
765
|
}
|
|
717
766
|
case 'user': {
|
|
718
767
|
const um = m;
|
|
768
|
+
// Detect a dropped remote-MCP session in any tool_result and re-handshake
|
|
769
|
+
// so the model's next tool call lands on a live session instead of
|
|
770
|
+
// 404-looping for the rest of the turn (the SDK won't auto-retry it).
|
|
771
|
+
const content = um.message?.['content'];
|
|
772
|
+
if (Array.isArray(content)) {
|
|
773
|
+
for (const block of content) {
|
|
774
|
+
if (block && typeof block === 'object' && block['type'] === 'tool_result'
|
|
775
|
+
&& MCP_SESSION_EXPIRED_RE.test(toolResultText(block['content']))) {
|
|
776
|
+
maybeReconnectRemoteMcp(q);
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
719
781
|
const event = { type: 'user', message: um.message };
|
|
720
782
|
if (handleEvent(event))
|
|
721
783
|
onEvent(event);
|