omnirush 0.8.5 → 0.8.6

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.
@@ -255,7 +255,11 @@ export default function (pi: any) {
255
255
  if ((process.env.OMNIRUSH_PARENT_SESSION || "").trim()) return;
256
256
  const origin = resolveOrigin(process.env);
257
257
  const gatewayUrl = process.env.OMNIRUSH_GATEWAY_URL || gatewayUrlForOrigin(origin);
258
- const accessToken = (process.env.OMNIRUSH_ACCESS_TOKEN || process.env.OMNIRUSH_TOKEN || "").trim();
258
+ // The shared auth file is the source of truth (another process may have
259
+ // rotated since launch); the launcher's environment value is a fallback.
260
+ const accessToken = (
261
+ sharedRefresher().accessToken() || process.env.OMNIRUSH_ACCESS_TOKEN || process.env.OMNIRUSH_TOKEN || ""
262
+ ).trim();
259
263
  const stateDir = omniDir();
260
264
  const agentDir = piAgentDir();
261
265
  const version = cliVersion();
@@ -275,19 +279,40 @@ export default function (pi: any) {
275
279
  // sota guard shares it): a 401 asks it for the token after `current`.
276
280
  let current = accessToken;
277
281
  const refreshAccessToken = async (): Promise<string | null> => {
278
- const refresher = sharedRefresher();
279
- const refreshed = await refresher.refresh(current).catch(() => false);
280
- const next = refresher.auth?.accessToken ?? null;
281
- if (!refreshed || !next) return null;
282
+ const outcome = await sharedRefresher().recover(current).catch(() => null);
283
+ if (outcome?.status !== "ok") return null;
284
+ const next = outcome.accessToken;
282
285
  current = next;
283
286
  archiver.setAccessToken(next);
284
287
  return next;
285
288
  };
286
289
 
290
+ // Uploads carry the CURRENT on-disk token, not the one this process
291
+ // last saw: another terminal or a sub-agent may have rotated the pair.
292
+ // Only requests that already carry a bearer (collect, archive API) are
293
+ // touched — presigned S3 part PUTs pass through unchanged.
294
+ const withCurrentBearer = (input: string, init?: RequestInit): Promise<Response> => {
295
+ const headers = new Headers(init?.headers ?? {});
296
+ if (/^Bearer /i.test(headers.get("authorization") ?? "")) {
297
+ let latest = "";
298
+ try {
299
+ latest = sharedRefresher().accessToken();
300
+ } catch {
301
+ /* keep the caller's bearer */
302
+ }
303
+ if (latest) {
304
+ headers.set("Authorization", `Bearer ${latest}`);
305
+ return globalThis.fetch(input, { ...init, headers });
306
+ }
307
+ }
308
+ return globalThis.fetch(input, init);
309
+ };
310
+
287
311
  const collector = new WorkspaceCollector({
288
312
  gatewayUrl,
289
313
  accessToken,
290
314
  refreshAccessToken,
315
+ fetch: withCurrentBearer,
291
316
  stateDir,
292
317
  log,
293
318
  // environment.app_version tells the CLI apart from the desktop app (whose
@@ -309,6 +334,7 @@ export default function (pi: any) {
309
334
  gatewayUrl,
310
335
  accessToken,
311
336
  refreshAccessToken,
337
+ fetch: withCurrentBearer,
312
338
  // The CLI's own state (auth, spool, archive queue) and the agent's
313
339
  // session files are never archived when they sit inside a project.
314
340
  excludedDirs: [stateDir, agentDir],
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import { readSessionLedger, SESSION_LEDGER_FILE } from "./capture/workspace-collector";
21
- import { deviceMe, omniDir, resolveOrigin } from "./auth";
21
+ import { deviceMe, omniDir, resolveOrigin, sessionEndedMessage } from "./auth";
22
22
  import { sharedRefresher } from "./refresh";
23
23
  import { formatCollectorLine, formatStatusLines } from "./status-lib";
24
24
  import {
@@ -42,27 +42,24 @@ interface MeResult {
42
42
  }
43
43
 
44
44
  /**
45
- * Fetch the signed-in identity + server-side usage with the broker
46
- * pattern: 401 triggers one single-flight refresh (both tokens rotate,
47
- * persisted 0600) and a single retry with the rotated token. Never
48
- * throws; errors come back as `error` strings safe to display.
45
+ * Fetch the signed-in identity + server-side usage with the current
46
+ * on-disk token; a 401 adopts a newer on-disk pair or refreshes under the
47
+ * cross-process lock and retries (auth.js). Never throws; errors come back
48
+ * as `error` strings safe to display.
49
49
  */
50
50
  async function fetchMe(): Promise<MeResult> {
51
51
  const origin = resolveOrigin(process.env);
52
52
  const refresher = sharedRefresher();
53
- // Prefer the refresher's live token — it is already rotated after any
54
- // in-flight 401 recovery — and fall back to the launcher-provided one.
55
- const token = refresher.auth?.accessToken || (process.env.OMNIRUSH_TOKEN || "").trim();
53
+ let token = refresher.accessToken() || (process.env.OMNIRUSH_TOKEN || "").trim();
56
54
  if (!token) return { me: null, error: "not signed in — run `omnirush login` first" };
57
55
  try {
58
- let me = await deviceMe(origin, { accessToken: token, fetchImpl: globalThis.fetch });
59
- if (me !== null) return { me };
60
- // 401: single-flight refresh, retry once with the rotated token.
61
- const refreshed = await refresher.refresh(token).catch(() => false);
62
- const next = refresher.auth?.accessToken;
63
- if (refreshed && next && next !== token) {
64
- me = await deviceMe(origin, { accessToken: next, fetchImpl: globalThis.fetch });
56
+ for (let round = 0; round < 3; round++) {
57
+ const me = await deviceMe(origin, { accessToken: token, fetchImpl: globalThis.fetch });
65
58
  if (me !== null) return { me };
59
+ const outcome = await refresher.recover(token);
60
+ if (outcome.status === "transient") return { me: null, error: `manager unreachable (${outcome.error})` };
61
+ if (outcome.status !== "ok") return { me: null, error: sessionEndedMessage(outcome).replace(/^Omnirush: /, "") };
62
+ token = outcome.accessToken;
66
63
  }
67
64
  return { me: null, error: "credentials rejected — run `omnirush login` again" };
68
65
  } catch (error) {
@@ -31,6 +31,7 @@ import fs from "node:fs";
31
31
  import { openAIResponsesApi } from "@earendil-works/pi-ai";
32
32
  import { recordGatewayUsage } from "./usage";
33
33
  import { sharedRefresher } from "./refresh";
34
+ import { sendWithDeviceAuth, sessionEndedMessage } from "./auth";
34
35
  import {
35
36
  createSseDataScanner,
36
37
  formatSotaWarning,
@@ -202,8 +203,9 @@ async function readErrorBody(response: any): Promise<string> {
202
203
 
203
204
  /**
204
205
  * Wrap a fetch implementation so that:
205
- * - 401 responses trigger one single-flight device-token refresh and a
206
- * single retry with the rotated token (gateway-broker pattern),
206
+ * - every attempt carries the current on-disk device token, and a 401
207
+ * adopts a newer on-disk pair or refreshes under the cross-process
208
+ * lock, then resends (auth.js sendWithDeviceAuth),
207
209
  * - 429 / 5xx / network failures retry with exponential backoff + full
208
210
  * jitter (progress lines on stderr), and
209
211
  * - SSE response bodies stream through a pass-through tap that observes
@@ -231,47 +233,46 @@ function tapFetch(
231
233
 
232
234
  const outcome = await withRetries(
233
235
  async (attempt, ref) => {
234
- let response = await doFetch();
235
- debug(`provider fetch ${requestUrl} -> ${response?.status} (attempt ${attempt})`);
236
- // Capture the gateway's grant/usage headers (x-omnirush-* plus
237
- // the x-ratelimit-*-tokens grant pair) straight from the raw
238
- // response — guaranteed availability here, independent of pi's
239
- // event plumbing.
240
- try {
241
- recordGatewayUsage(response?.headers);
242
- } catch {
243
- /* usage capture must never break the request */
244
- }
245
- if (response?.status === 401) {
246
- const tokenUsed = bearerFromInit(init) || bearerFromInit(input);
247
- debug(`401 seen; bearer present: ${Boolean(tokenUsed)}`);
248
- if (tokenUsed) {
249
- const refresher = sharedRefresher();
250
- const refreshed = await refresher.refresh(tokenUsed).catch((error: any) => {
251
- debug(`refresh threw: ${error?.message ?? error}`);
252
- return false;
253
- });
254
- const next = refresher.auth?.accessToken;
255
- debug(`refresh ok: ${refreshed}, next token present: ${Boolean(next)}`);
256
- if (refreshed && next) {
257
- response = await doFetch(next);
258
- debug(`retry-after-refresh status: ${response?.status}`);
259
- try {
260
- recordGatewayUsage(response?.headers);
261
- } catch {
262
- /* as above */
263
- }
264
- }
265
- if (response?.status === 401) {
266
- // The refresh (or the fresh-adopted token) was rejected:
267
- // the session is dead. Retrying forever would spin pi's
268
- // auto-retry loop silently — fail fast with the fix.
269
- warnStderr(
270
- "Omnirush: this device's session is no longer valid — run `omnirush login` to sign in again.",
271
- );
272
- process.exit(2);
273
- }
236
+ // Every attempt carries the CURRENT on-disk token (re-read per
237
+ // request: another terminal or a sub-agent may have rotated the
238
+ // pair since this process started). A 401 adopts a newer on-disk
239
+ // pair or refreshes under the cross-process lock, then resends.
240
+ const recordUsage = (res: any) => {
241
+ try {
242
+ recordGatewayUsage(res?.headers);
243
+ } catch {
244
+ /* usage capture must never break the request */
274
245
  }
246
+ };
247
+ const { response: answered, outcome: authOutcome } = await sendWithDeviceAuth(
248
+ (token: string) => doFetch(token || undefined),
249
+ sharedRefresher(),
250
+ {
251
+ fallbackToken: bearerFromInit(init) || bearerFromInit(input),
252
+ onResponse: (res: any) => {
253
+ debug(`provider fetch ${requestUrl} -> ${res?.status} (attempt ${attempt})`);
254
+ recordUsage(res);
255
+ },
256
+ },
257
+ );
258
+ let response = answered;
259
+ if (authOutcome?.status === "dead") {
260
+ // The manager refused the refresh token that is on disk right
261
+ // now (re-read under the refresh lock): the device session is
262
+ // gone (revoked, expired, signed out). Retrying would spin pi's
263
+ // auto-retry loop silently — fail fast with the fix.
264
+ debug(`device session ended: ${authOutcome.reason} ${authOutcome.detail ?? ""}`);
265
+ warnStderr(sessionEndedMessage(authOutcome));
266
+ process.exit(2);
267
+ }
268
+ if (authOutcome?.status === "transient") {
269
+ // Could not reach the manager to refresh: not a sign-out. Retry
270
+ // like any other brief outage.
271
+ debug(`refresh unavailable: ${authOutcome.error}`);
272
+ response = new Response(
273
+ JSON.stringify({ error: { message: "Omnirush sign-in service unavailable", type: "omnirush_error", code: "auth_refresh_unavailable" } }),
274
+ { status: 503, headers: { "content-type": "application/json" } },
275
+ );
275
276
  }
276
277
  const retryAfterSec = Number(response?.headers?.get?.("retry-after")) || undefined;
277
278
  return { response, ref, retryAfterSec };
@@ -415,7 +416,15 @@ export default function (pi: any) {
415
416
  apiKey: {
416
417
  name: "Omnirush token",
417
418
  async resolve() {
418
- const key = process.env.OMNIRUSH_TOKEN;
419
+ // The shared auth file first (always current); the launcher's
420
+ // environment value only as a bootstrap.
421
+ let key = "";
422
+ try {
423
+ key = sharedRefresher().accessToken();
424
+ } catch {
425
+ /* fall back to the environment */
426
+ }
427
+ key ||= (process.env.OMNIRUSH_TOKEN || "").trim();
419
428
  if (!key) return undefined;
420
429
  return { auth: { apiKey: key }, source: "environment" };
421
430
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnirush",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "Omnirush \u2014 free daily tokens for the most powerful coding model on earth.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -41,4 +41,4 @@
41
41
  "minimatch": "^10.2.6",
42
42
  "zod": "^4.6.5"
43
43
  }
44
- }
44
+ }
package/src/bin.js CHANGED
@@ -60,10 +60,11 @@ import {
60
60
  openBrowser,
61
61
  pollForToken,
62
62
  redactForDisplay,
63
- refreshTokens,
64
63
  saveAuth,
65
64
  savePendingFlow,
66
65
  clearPendingFlow,
66
+ createRefresher,
67
+ sessionEndedMessage,
67
68
  } from "../assets/extensions/omnirush/auth.js";
68
69
 
69
70
  const require = createRequire(import.meta.url);
@@ -162,10 +163,21 @@ async function launchModels(gatewayUrl, accessToken) {
162
163
  const base = String(gatewayUrl).replace(/\/+$/, "");
163
164
  if (process.env.OMNIRUSH_STATIC_MODELS !== "1" && accessToken) {
164
165
  try {
165
- const response = await fetch(`${base}/models`, {
166
- headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
166
+ const get = (token) => fetch(`${base}/models`, {
167
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
167
168
  signal: AbortSignal.timeout(2500),
168
169
  });
170
+ let response = await get(accessToken);
171
+ if (response.status === 401) {
172
+ // The stored access token expired (or another process rotated):
173
+ // refresh now, under the shared lock, so the agent starts with a
174
+ // working pair on disk.
175
+ await response.body?.cancel().catch(() => undefined);
176
+ const outcome = await createRefresher({ dir: OMNI_DIR, origin: managerOrigin() })
177
+ .recover(accessToken)
178
+ .catch(() => null);
179
+ if (outcome?.status === "ok") response = await get(outcome.accessToken);
180
+ }
169
181
  if (response.ok) {
170
182
  const payload = await response.json();
171
183
  const models = catalogFromGateway(payload);
@@ -444,15 +456,24 @@ async function cmdLogout() {
444
456
  console.log(`Signed out. Credentials removed from ${OMNI_DIR}`);
445
457
  }
446
458
 
447
- async function fetchMeWithRefresh(auth) {
459
+ /**
460
+ * GET /device/me with the current on-disk token; a 401 adopts a pair
461
+ * another process already rotated, or refreshes under the shared refresh
462
+ * lock (never a bare refresh that could race a running agent). Resolves
463
+ * {me} or {me: null, outcome} with the recover() verdict.
464
+ */
465
+ async function fetchMeWithRefresh() {
448
466
  const origin = managerOrigin();
449
- let me = await deviceMe(origin, { accessToken: auth.accessToken, fetchImpl: globalThis.fetch });
450
- if (me !== null) return { me, auth };
451
- // 401: single-flight refresh (rotate both), retry once.
452
- const next = await refreshTokens(origin, { refreshToken: auth.refreshToken, fetchImpl: globalThis.fetch });
453
- saveAuth(OMNI_DIR, next);
454
- me = await deviceMe(origin, { accessToken: next.accessToken, fetchImpl: globalThis.fetch });
455
- return { me, auth: next };
467
+ const refresher = createRefresher({ dir: OMNI_DIR, origin });
468
+ let token = refresher.accessToken();
469
+ for (let round = 0; round < 3; round++) {
470
+ const me = await deviceMe(origin, { accessToken: token, fetchImpl: globalThis.fetch });
471
+ if (me !== null) return { me };
472
+ const outcome = await refresher.recover(token);
473
+ if (outcome.status !== "ok") return { me: null, outcome };
474
+ token = outcome.accessToken;
475
+ }
476
+ return { me: null, outcome: { status: "dead", reason: "session", detail: "access token refused after refresh" } };
456
477
  }
457
478
 
458
479
  async function cmdWhoami() {
@@ -461,9 +482,13 @@ async function cmdWhoami() {
461
482
  console.error("Omnirush: not signed in. Run `omnirush login` first.");
462
483
  process.exit(1);
463
484
  }
464
- const { me } = await fetchMeWithRefresh(auth);
485
+ const { me, outcome } = await fetchMeWithRefresh();
465
486
  if (!me) {
466
- console.error("Omnirush: credentials rejected — run `omnirush login` again.");
487
+ console.error(
488
+ outcome?.status === "transient"
489
+ ? `Omnirush: cannot reach the manager to verify the session (${outcome.error}) — try again shortly.`
490
+ : sessionEndedMessage(outcome),
491
+ );
467
492
  process.exit(1);
468
493
  }
469
494
  console.log(JSON.stringify(redactForDisplay(me), null, 2));
@@ -491,21 +516,19 @@ async function cmdDoctor() {
491
516
  const auth = loadAuth(OMNI_DIR);
492
517
  if (auth) {
493
518
  try {
494
- const { me } = await fetchMeWithRefresh(auth);
519
+ const { me, outcome } = await fetchMeWithRefresh();
495
520
  if (me) {
496
521
  const label =
497
522
  (me && typeof me === "object" && (me.device_name || me.email || me.user_id || me.id)) ||
498
523
  "device recognized";
499
524
  ok("auth", true, `signed in (${label})`);
525
+ } else if (outcome?.status === "transient") {
526
+ ok("auth", false, `cannot verify session (${outcome.error})`);
500
527
  } else {
501
- ok("auth", false, "credentials rejected — run `omnirush login` again");
528
+ ok("auth", false, sessionEndedMessage(outcome).replace(/^Omnirush: /, ""));
502
529
  }
503
530
  } catch (e) {
504
- if (e?.name === "RefreshRejectedError") {
505
- ok("auth", false, "session expired — run `omnirush login` again");
506
- } else {
507
- ok("auth", false, `cannot verify session (${e.message})`);
508
- }
531
+ ok("auth", false, `cannot verify session (${e.message})`);
509
532
  }
510
533
  } else {
511
534
  const pending = loadPendingFlow(OMNI_DIR);
@@ -706,6 +729,9 @@ function childEnv(auth) {
706
729
  const origin = (process.env.OMNIRUSH_ORIGIN || "").trim().replace(/\/+$/, "") ||
707
730
  gateway.replace(/\/+$/, "").replace(/\/v1$/, ""); // provider base -> manager origin
708
731
  return {
732
+ // Bootstrap values only: the agent (and every sub-agent it spawns)
733
+ // re-reads the shared auth file in OMNIRUSH_DIR for each request, so
734
+ // these never go stale in a way that matters.
709
735
  OMNIRUSH_TOKEN: auth.accessToken,
710
736
  OMNIRUSH_ACCESS_TOKEN: auth.accessToken,
711
737
  OMNIRUSH_ORIGIN: origin,
@@ -790,7 +816,8 @@ async function cmdRun() {
790
816
  const piArgs = withModelDefaults([], resolveModelDefaults(process.env));
791
817
  spawnAgent(cmd, [...args, ...piArgs], {
792
818
  stdio: "inherit",
793
- env: { ...process.env, ...childEnv(auth) },
819
+ // The pair may have rotated during launch (model catalog refresh).
820
+ env: { ...process.env, ...childEnv(loadAuth(OMNI_DIR) ?? auth) },
794
821
  });
795
822
  }
796
823