@gakim-digital/dexter-bridge 0.12.0 → 0.12.1

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/package.json +1 -1
  2. package/src/framerTools.js +48 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Local bridge for InstaWebAI and Dexter — runs Codex, Claude Code, or OpenCode on your machine.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -170,6 +170,14 @@ function toolError(code, message, statusCode = 400) {
170
170
  return Object.assign(new Error(message), { code, statusCode });
171
171
  }
172
172
 
173
+ // Framer answers "Framer stopped answering…" when a program gets no reply
174
+ // within its own 60-second limit; the MCP SDK reports its timeout as -32001.
175
+ const FRAMER_TIMEOUT_PATTERN = /stopped answering|request timed out/i;
176
+
177
+ function isFramerTimeout(error) {
178
+ return error?.code === -32001 || FRAMER_TIMEOUT_PATTERN.test(String(error?.message || ''));
179
+ }
180
+
173
181
  function codeOutsideLiterals(code) {
174
182
  let visible = '';
175
183
  for (const token of jsTokens(String(code || ''))) {
@@ -547,6 +555,10 @@ export function createFramerAgentToolRuntime({
547
555
  let closed = false;
548
556
  const abortController = new AbortController();
549
557
  let pendingRepair = null;
558
+ let consecutiveTimeouts = 0;
559
+ let fatalError = null;
560
+ let signalFatal;
561
+ const fatalInfrastructure = new Promise((resolve) => { signalFatal = resolve; });
550
562
 
551
563
  // Access tokens last minutes; the API refreshes them. A token is reused
552
564
  // until a minute before it expires, and replaced at once when Framer
@@ -613,6 +625,32 @@ export function createFramerAgentToolRuntime({
613
625
  }
614
626
  }
615
627
 
628
+ // When Framer stops running programs, every call waits out Framer's
629
+ // 60-second limit. Framer sometimes stalls once and recovers, so two
630
+ // timeouts in a row, counting the harness's own setup read, end the run
631
+ // instead of letting it wait through one timeout after another.
632
+ function noteFramerAnswer(error) {
633
+ if (!isFramerTimeout(error)) {
634
+ consecutiveTimeouts = 0;
635
+ return;
636
+ }
637
+ consecutiveTimeouts += 1;
638
+ if (consecutiveTimeouts < 2) return;
639
+ if (!fatalError) {
640
+ fatalError = Object.assign(
641
+ toolError(
642
+ 'FRAMER_NOT_RESPONDING',
643
+ mutationCalls > 0
644
+ ? 'Framer stopped responding, so Dexter stopped. The changes made before that were kept; try again in a minute.'
645
+ : 'Framer isn’t responding right now, so Dexter stopped. Nothing was changed; try again in a minute.',
646
+ 503,
647
+ ),
648
+ { retryable: true },
649
+ );
650
+ signalFatal(fatalError);
651
+ }
652
+ }
653
+
616
654
  function recordCall(name, category, status, extra = {}) {
617
655
  toolCallCount += 1;
618
656
  toolCalls.push({ name, category, status, durationMs: extra.durationMs || 0, ...extra });
@@ -729,6 +767,7 @@ export function createFramerAgentToolRuntime({
729
767
 
730
768
  async function invokeUnsafe(name, args = {}) {
731
769
  if (closed) throw toolError('FRAMER_AGENT_RUNTIME_CLOSED', 'This Framer run has finished.');
770
+ if (fatalError) throw fatalError;
732
771
  if (!definitions.some((tool) => tool.name === name)) {
733
772
  throw toolError('FRAMER_AGENT_TOOL_UNKNOWN', `Unknown Framer tool ${name}.`);
734
773
  }
@@ -902,6 +941,7 @@ export function createFramerAgentToolRuntime({
902
941
  throw toolError('FRAMER_AGENT_TOOL_UNKNOWN', `Unknown Framer tool ${name}.`);
903
942
  }
904
943
  recordCall(name, category, 'succeeded', { ...callExtra, durationMs: Date.now() - startedAt });
944
+ noteFramerAnswer(null);
905
945
  const nudge = category === 'read' ? firstEditNudge() : null;
906
946
  if (nudge) payload = { ...payload, nextStep: nudge };
907
947
  reportActivity({
@@ -926,7 +966,8 @@ export function createFramerAgentToolRuntime({
926
966
  message: error?.message,
927
967
  durationMs: Date.now() - startedAt,
928
968
  });
929
- throw error;
969
+ noteFramerAnswer(error);
970
+ throw fatalError || error;
930
971
  }
931
972
  }
932
973
 
@@ -975,7 +1016,10 @@ export function createFramerAgentToolRuntime({
975
1016
  const [pages, contextPages, activePage] = await Promise.all([
976
1017
  readDocsTolerant(GUIDANCE_PATHS),
977
1018
  readDocsTolerant([CONTEXT_PATH]),
978
- readActivePageOutline().catch(() => ''),
1019
+ readActivePageOutline().catch((error) => {
1020
+ noteFramerAnswer(error);
1021
+ return '';
1022
+ }),
979
1023
  ]);
980
1024
  const inventory = [
981
1025
  stripGuidance(contextPages.map((page) => page.content).join('\n\n')),
@@ -1234,8 +1278,8 @@ export function createFramerAgentToolRuntime({
1234
1278
  files: [...guidance.files],
1235
1279
  },
1236
1280
  }),
1237
- fatalInfrastructureError: () => null,
1238
- waitForFatalInfrastructure: () => new Promise(() => undefined),
1281
+ fatalInfrastructureError: () => fatalError,
1282
+ waitForFatalInfrastructure: () => fatalInfrastructure,
1239
1283
  close: async ({ waitForPending = true } = {}) => {
1240
1284
  closed = true;
1241
1285
  abortController.abort();