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

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 +25 -21
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -316,35 +316,39 @@ export const RateLimitFallback = async ({ client, directory }) => {
316
316
  parts: parts,
317
317
  model: { providerID: nextModel.providerID, modelID: nextModel.modelID },
318
318
  };
319
- // CRITICAL PATH: abort → promptAsync with NO delay between them.
319
+ // CRITICAL PATH: promptAsync BEFORE abort.
320
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.
321
+ // In headless mode (opencode run), abort promptAsync fails because:
322
+ // 1. abort triggers server dispose sequence
323
+ // 2. promptAsync is accepted and message created
324
+ // 3. Server starts processing (busy) but dispose interrupts it (idle)
325
+ // 4. server.instance.disposed — all within ~6ms
325
326
  //
326
- // The await on promptAsync waits for the HTTP round-trip (server acknowledgment),
327
- // NOT for prompt completion generation runs asynchronously on the server.
327
+ // By sending promptAsync FIRST, the server knows there is pending work
328
+ // before abort triggers the dispose check. When abort cancels the retry
329
+ // loop and the session goes idle, the server should process the queued
330
+ // prompt instead of disposing.
328
331
  //
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.
332
+ // promptAsync: HTTP POST /session/{id}/prompt_async 204 (SDK sdk.gen.js).
333
+ // prompt (sync): blocks until generation completes do NOT use.
334
+ const t0 = Date.now();
335
+ await client.session.promptAsync({
336
+ path: { id: sessionID },
337
+ body: promptBody,
338
+ });
339
+ logToFile(`promptAsync completed for session ${sessionID} (${Date.now() - t0}ms) with model ${nextModel.providerID}/${nextModel.modelID}`);
340
+ const t1 = Date.now();
331
341
  try {
332
342
  await client.session.abort({ path: { id: sessionID } });
333
- logToFile(`abort succeeded for session ${sessionID}`);
343
+ logToFile(`abort succeeded for session ${sessionID} (${Date.now() - t1}ms, total ${Date.now() - t0}ms)`);
334
344
  }
335
345
  catch (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`);
346
+ logToFile(`abort failed (${Date.now() - t1}ms): ${abortErr}`);
340
347
  }
341
- await client.session.promptAsync({
342
- path: { id: sessionID },
343
- body: promptBody,
344
- });
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 is best-effort notification. The toast() function (line ~185) has
349
+ // built-in fallback: showToast failure → app.log. After promptAsync the
350
+ // server may already be disposing, so both showToast and app.log could fail.
351
+ // The outer .catch() ensures even total toast() failure never blocks or throws.
348
352
  toast("Fallback Active", `Now using ${nextModel.modelID}`, "success").catch(() => { });
349
353
  retryState.delete(stateKey);
350
354
  // Clear fallback flag to allow next fallback if needed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "OpenCode plugin that automatically switches to fallback models when rate limited",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",