@gakim-digital/dexter-bridge 0.5.7 → 0.5.8

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 (3) hide show
  1. package/package.json +1 -1
  2. package/src/agent.js +74 -38
  3. package/src/api.js +10 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "Local Companion bridge for the Dexter Framer plugin — runs Codex or Claude Code on your machine.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -108,45 +108,81 @@ function nowIso() {
108
108
  return new Date().toISOString();
109
109
  }
110
110
 
111
+ const TERMINAL_EVENT_TYPES = new Set(['done', 'error']);
112
+ const TERMINAL_EVENT_MAX_ATTEMPTS = 5;
113
+
114
+ function waitForRetry(delayMs) {
115
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
116
+ }
117
+
118
+ function terminalEventRetryDelay(error, attempt) {
119
+ const status = Number(error?.status);
120
+ const retryable =
121
+ !Number.isFinite(status)
122
+ || status === 408
123
+ || status === 429
124
+ || status >= 500;
125
+ if (!retryable) return null;
126
+ if (Number.isFinite(error?.retryAfterMs) && error.retryAfterMs > 0) {
127
+ return Math.min(65_000, Math.max(250, error.retryAfterMs));
128
+ }
129
+ return Math.min(8_000, 500 * (2 ** (attempt - 1)));
130
+ }
131
+
111
132
  function createEventPoster({ apiBaseUrl, deviceToken, run, fetchImpl, trace }) {
112
133
  return async function send(type, payload = {}) {
113
134
  const eventId = `${type}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
114
135
  const started = Date.now();
115
- trace?.info('event_post_start', {
116
- type,
117
- eventId,
118
- payload: summarizePayload(payload),
119
- });
120
- try {
121
- const response = await postRunEvent(apiBaseUrl, {
122
- deviceToken,
123
- runId: run.runId,
124
- fetchImpl,
125
- event: {
126
- type,
127
- eventId,
128
- payload,
129
- },
130
- });
131
- trace?.info('event_post_done', {
132
- type,
133
- eventId,
134
- durationMs: Date.now() - started,
135
- accepted: response?.accepted,
136
- status: response?.status,
137
- hasToolResult: Boolean(response?.toolResult),
138
- toolResult: summarizeToolResult(response?.toolResult),
139
- });
140
- return response;
141
- } catch (error) {
142
- trace?.error('event_post_failed', {
136
+ const maxAttempts = TERMINAL_EVENT_TYPES.has(type)
137
+ ? TERMINAL_EVENT_MAX_ATTEMPTS
138
+ : 1;
139
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
140
+ trace?.info('event_post_start', {
143
141
  type,
144
142
  eventId,
145
- durationMs: Date.now() - started,
146
- error: errorMeta(error),
143
+ attempt,
144
+ payload: summarizePayload(payload),
147
145
  });
148
- throw error;
146
+ try {
147
+ const response = await postRunEvent(apiBaseUrl, {
148
+ deviceToken,
149
+ runId: run.runId,
150
+ fetchImpl,
151
+ event: {
152
+ type,
153
+ eventId,
154
+ payload,
155
+ },
156
+ });
157
+ trace?.info('event_post_done', {
158
+ type,
159
+ eventId,
160
+ attempt,
161
+ durationMs: Date.now() - started,
162
+ accepted: response?.accepted,
163
+ status: response?.status,
164
+ hasToolResult: Boolean(response?.toolResult),
165
+ toolResult: summarizeToolResult(response?.toolResult),
166
+ });
167
+ return response;
168
+ } catch (error) {
169
+ const retryDelayMs =
170
+ attempt < maxAttempts
171
+ ? terminalEventRetryDelay(error, attempt)
172
+ : null;
173
+ trace?.error('event_post_failed', {
174
+ type,
175
+ eventId,
176
+ attempt,
177
+ durationMs: Date.now() - started,
178
+ retryDelayMs,
179
+ error: errorMeta(error),
180
+ });
181
+ if (retryDelayMs === null) throw error;
182
+ await waitForRetry(retryDelayMs);
183
+ }
149
184
  }
185
+ throw new Error(`Unable to deliver ${type} event.`);
150
186
  };
151
187
  }
152
188
 
@@ -887,7 +923,7 @@ class CompanionRunCancelledError extends Error {
887
923
  async function callProviderAdapter(adapter, input, {
888
924
  send,
889
925
  trace,
890
- controlPollMs = 250,
926
+ controlPollMs = 5_000,
891
927
  } = {}) {
892
928
  if (!adapter || typeof adapter.runModelTurn !== 'function') {
893
929
  throw new Error('The selected provider adapter cannot execute model turns.');
@@ -1209,9 +1245,9 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
1209
1245
  trace: options.trace,
1210
1246
  controlPollMs: boundedDurationMs(
1211
1247
  options.controlPollMs ?? options.env?.DEXTER_BRIDGE_CONTROL_POLL_MS,
1212
- 250,
1213
- 50,
1214
- 1000,
1248
+ 5_000,
1249
+ 1_000,
1250
+ 30_000,
1215
1251
  ),
1216
1252
  };
1217
1253
  let result;
@@ -1253,9 +1289,9 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
1253
1289
  const monitorAbort = new AbortController();
1254
1290
  const controlPollMs = boundedDurationMs(
1255
1291
  options.controlPollMs ?? options.env?.DEXTER_BRIDGE_CONTROL_POLL_MS,
1256
- 250,
1257
- 50,
1258
- 1000,
1292
+ 5_000,
1293
+ 1_000,
1294
+ 30_000,
1259
1295
  );
1260
1296
  let cliFinished = false;
1261
1297
  let cliCancelled = false;
package/src/api.js CHANGED
@@ -7,11 +7,12 @@ import {
7
7
  } from './config.js';
8
8
 
9
9
  export class DexterBridgeApiError extends Error {
10
- constructor(message, status, body) {
10
+ constructor(message, status, body, retryAfterMs) {
11
11
  super(message);
12
12
  this.name = 'DexterBridgeApiError';
13
13
  this.status = status;
14
14
  this.body = body;
15
+ this.retryAfterMs = retryAfterMs;
15
16
  }
16
17
  }
17
18
 
@@ -50,10 +51,18 @@ export async function requestJson(apiBaseUrl, path, {
50
51
  });
51
52
  const parsed = await response.json().catch(() => null);
52
53
  if (!response.ok || parsed?.success === false) {
54
+ const retryAfterHeader = Number(response.headers?.get?.('retry-after'));
55
+ const retryAfterSeconds = Number(parsed?.retryAfterSeconds);
56
+ const retryAfterMs = Number.isFinite(retryAfterHeader) && retryAfterHeader > 0
57
+ ? retryAfterHeader * 1000
58
+ : Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0
59
+ ? retryAfterSeconds * 1000
60
+ : undefined;
53
61
  throw new DexterBridgeApiError(
54
62
  parsed?.error || parsed?.message || `Dexter API request failed with status ${response.status}`,
55
63
  response.status,
56
64
  parsed,
65
+ retryAfterMs,
57
66
  );
58
67
  }
59
68
  return parsed;