@azumag/opencode-rate-limit-fallback 1.0.14 → 1.0.16

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/index.js +20 -11
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -316,27 +316,36 @@ export const RateLimitFallback = async ({ client, directory }) => {
316
316
  parts: parts,
317
317
  model: { providerID: nextModel.providerID, modelID: nextModel.modelID },
318
318
  };
319
- // Abort to cancel the retry loop
319
+ // CRITICAL PATH: abort promptAsync with NO delay between them.
320
+ //
321
+ // In headless mode (opencode run), the server disposes within milliseconds
322
+ // after session goes idle (observed ~8ms in production logs — this is not
323
+ // a guaranteed bound, just empirically observed). Any delay (setTimeout,
324
+ // awaited toast, etc.) means promptAsync arrives after the server is dead.
325
+ //
326
+ // The await on promptAsync waits for the HTTP round-trip (server acknowledgment),
327
+ // NOT for prompt completion — generation runs asynchronously on the server.
328
+ //
329
+ // Do NOT use prompt() (sync) here — it triggers the abort flag race condition
330
+ // in TUI mode, causing the new prompt to be immediately interrupted.
320
331
  try {
321
332
  await client.session.abort({ path: { id: sessionID } });
322
333
  logToFile(`abort succeeded for session ${sessionID}`);
323
334
  }
324
335
  catch (abortErr) {
325
- logToFile(`abort failed (non-critical): ${abortErr}`);
336
+ // If abort fails, the session may still be in its retry loop.
337
+ // We still send promptAsync as best-effort: when the retry loop eventually
338
+ // completes (timeout or success), the queued prompt should be processed.
339
+ logToFile(`abort failed: ${abortErr} — sending promptAsync as best-effort`);
326
340
  }
327
- // Wait for abort to fully complete before sending new prompt.
328
- // Toast alone (~50ms) is insufficient — the abort flag may still be set,
329
- // causing the new prompt to be immediately interrupted.
330
- await new Promise(resolve => setTimeout(resolve, 500));
331
- toast("Retrying", `Using ${nextModel.providerID}/${nextModel.modelID}`, "info").catch(() => { });
332
- // Use promptAsync — returns immediately, compatible with both TUI and headless modes.
333
- // prompt (sync) causes race condition: abort flag not yet cleared → prompt interrupted.
334
341
  await client.session.promptAsync({
335
342
  path: { id: sessionID },
336
343
  body: promptBody,
337
344
  });
338
- logToFile(`promptAsync sent successfully for session ${sessionID} with model ${nextModel.providerID}/${nextModel.modelID}`);
339
- toast("Fallback Successful", `Now using ${nextModel.modelID}`, "success").catch(() => { });
345
+ logToFile(`promptAsync sent for session ${sessionID} with model ${nextModel.providerID}/${nextModel.modelID}`);
346
+ // Toasts are fire-and-forget: placed AFTER the critical path so they cannot
347
+ // interfere with the abort→promptAsync timing, even if they fail in headless.
348
+ toast("Fallback Active", `Now using ${nextModel.modelID}`, "success").catch(() => { });
340
349
  retryState.delete(stateKey);
341
350
  // Clear fallback flag to allow next fallback if needed
342
351
  fallbackInProgress.delete(sessionID);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "OpenCode plugin that automatically switches to fallback models when rate limited",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",