@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.
Files changed (2) hide show
  1. package/dist/claude.js +63 -1
  2. 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
- for await (const m of query({ prompt: promptStream(promptMessages), options })) {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.60.0",
3
+ "version": "0.61.0",
4
4
  "description": "Run 1Presence on your Mac and use your Claude.ai Pro subscription from any device",
5
5
  "type": "module",
6
6
  "bin": {