@blockrun/franklin 3.15.47 → 3.15.48

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.
@@ -238,9 +238,13 @@ function buildExecute(deps) {
238
238
  headers,
239
239
  body,
240
240
  });
241
- // Handle x402 payment
241
+ // Handle x402 payment. Lifted out of the inner block so the polling
242
+ // path below can reuse the signed headers — every poll request
243
+ // re-presents the same authorization (the gateway settles on the
244
+ // first completed poll, same contract as videogen.ts:251).
245
+ let paymentHeaders = null;
242
246
  if (response.status === 402) {
243
- const paymentHeaders = await signPayment(response, chain, endpoint);
247
+ paymentHeaders = await signPayment(response, chain, endpoint);
244
248
  if (!paymentHeaders) {
245
249
  return { output: 'Payment failed. Check wallet balance with: franklin balance', isError: true };
246
250
  }
@@ -255,7 +259,66 @@ function buildExecute(deps) {
255
259
  const errText = await response.text().catch(() => '');
256
260
  return { output: `Image generation failed (${response.status}): ${errText.slice(0, 200)}`, isError: true };
257
261
  }
258
- const result = await response.json();
262
+ let result = await response.json();
263
+ // Async path: gateway returns HTTP 202 (Accepted, queued) + a poll_url
264
+ // when the upstream image model takes longer than the inline budget
265
+ // (gpt-image-1/-2 routinely exceed 30s). Verified 2026-05-04 from
266
+ // Cloud Run logs — five back-to-back ImageGen calls that the agent
267
+ // saw as "No image data returned from API" had all returned 202 +
268
+ // queued status; payment was settled but the result was still
269
+ // generating async. Without polling here, the agent burns retries
270
+ // (each charged) and the image silently completes elsewhere with
271
+ // no way for Franklin to retrieve it. Mirror videogen.ts's
272
+ // pollUntilReady contract: same x-payment header on each poll.
273
+ if (response.status === 202 && result.poll_url) {
274
+ const origin = new URL(apiUrl).origin;
275
+ const pollEndpoint = result.poll_url.startsWith('http')
276
+ ? result.poll_url
277
+ : `${origin}${result.poll_url}`;
278
+ const pollHeaders = paymentHeaders ? { ...headers, ...paymentHeaders } : headers;
279
+ // Replace the POST timeout with a longer poll deadline. Image
280
+ // generation routinely completes within 1–3 min once queued; the
281
+ // 5 min ceiling matches videogen's POLL_MAX_WAIT_MS scale.
282
+ clearTimeout(timeout);
283
+ const pollDeadline = Date.now() + 5 * 60 * 1000;
284
+ let polled = null;
285
+ while (Date.now() < pollDeadline) {
286
+ if (controller.signal.aborted)
287
+ throw new Error('aborted');
288
+ await new Promise(r => setTimeout(r, 3_000));
289
+ const pollResp = await fetch(pollEndpoint, {
290
+ method: 'GET',
291
+ headers: pollHeaders,
292
+ signal: controller.signal,
293
+ });
294
+ if (pollResp.status === 202)
295
+ continue; // still queued
296
+ if (pollResp.status === 429 || pollResp.status >= 500)
297
+ continue; // transient
298
+ if (pollResp.ok) {
299
+ polled = await pollResp.json().catch(() => null);
300
+ if (polled && (polled.status === 'completed' || polled.data?.[0]))
301
+ break;
302
+ if (polled && polled.status === 'failed') {
303
+ return {
304
+ output: `Image generation failed upstream: ${JSON.stringify(polled.error ?? polled).slice(0, 240)}`,
305
+ isError: true,
306
+ };
307
+ }
308
+ continue;
309
+ }
310
+ const text = await pollResp.text().catch(() => '');
311
+ return { output: `Image poll failed (${pollResp.status}): ${text.slice(0, 200)}`, isError: true };
312
+ }
313
+ if (!polled || !polled.data?.[0]) {
314
+ return {
315
+ output: `Image generation queued but did not complete within 5 minutes. Payment was settled when the gateway accepted the job (HTTP 202). ` +
316
+ `If this keeps happening, the upstream image model is overloaded — try a smaller / faster model or retry later.`,
317
+ isError: true,
318
+ };
319
+ }
320
+ result = polled;
321
+ }
259
322
  const imageData = result.data?.[0];
260
323
  if (!imageData) {
261
324
  // Some gateways return 200 with an `error` / `message` field for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.47",
3
+ "version": "3.15.48",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {