omnirush 0.8.4 → 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.
@@ -57,6 +57,7 @@ import {
57
57
  type PiEntry,
58
58
  } from "./pi-engine";
59
59
  import { sharedRefresher } from "./refresh";
60
+ import { AGENTS_HOOK, type AgentsHook } from "./agents-lib";
60
61
 
61
62
  /** info.agent of the root session's messages (a sub-agent's is its role). */
62
63
  export const ROOT_AGENT = "omnirush-cli";
@@ -254,7 +255,11 @@ export default function (pi: any) {
254
255
  if ((process.env.OMNIRUSH_PARENT_SESSION || "").trim()) return;
255
256
  const origin = resolveOrigin(process.env);
256
257
  const gatewayUrl = process.env.OMNIRUSH_GATEWAY_URL || gatewayUrlForOrigin(origin);
257
- 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();
258
263
  const stateDir = omniDir();
259
264
  const agentDir = piAgentDir();
260
265
  const version = cliVersion();
@@ -274,19 +279,40 @@ export default function (pi: any) {
274
279
  // sota guard shares it): a 401 asks it for the token after `current`.
275
280
  let current = accessToken;
276
281
  const refreshAccessToken = async (): Promise<string | null> => {
277
- const refresher = sharedRefresher();
278
- const refreshed = await refresher.refresh(current).catch(() => false);
279
- const next = refresher.auth?.accessToken ?? null;
280
- 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;
281
285
  current = next;
282
286
  archiver.setAccessToken(next);
283
287
  return next;
284
288
  };
285
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
+
286
311
  const collector = new WorkspaceCollector({
287
312
  gatewayUrl,
288
313
  accessToken,
289
314
  refreshAccessToken,
315
+ fetch: withCurrentBearer,
290
316
  stateDir,
291
317
  log,
292
318
  // environment.app_version tells the CLI apart from the desktop app (whose
@@ -308,6 +334,7 @@ export default function (pi: any) {
308
334
  gatewayUrl,
309
335
  accessToken,
310
336
  refreshAccessToken,
337
+ fetch: withCurrentBearer,
311
338
  // The CLI's own state (auth, spool, archive queue) and the agent's
312
339
  // session files are never archived when they sit inside a project.
313
340
  excludedDirs: [stateDir, agentDir],
@@ -403,6 +430,32 @@ export default function (pi: any) {
403
430
  return session;
404
431
  };
405
432
 
433
+ /**
434
+ * The sub-agent tree's messages since their checkpoints. Background
435
+ * sub-agents keep running past the turn that started them: each later
436
+ * settle (and the session's end) records what they did since.
437
+ */
438
+ const captureChildren = async (state: NonNullable<typeof session>, room: (bytes: number) => void): Promise<void> => {
439
+ const { id } = state;
440
+ try {
441
+ await captureChildSessions({
442
+ collector,
443
+ rootSessionId: id,
444
+ parentSessionId: id,
445
+ parentEntries: state.ctx?.sessionManager?.getBranch?.() ?? [],
446
+ depth: 1,
447
+ sessionDirs: sessionDirs(state.ctx),
448
+ checkpoints: await collector.childCheckpoints(id),
449
+ known: new Set(await collector.childSessionIds(id)),
450
+ visited: new Set([id]),
451
+ seenFiles: state.seenFiles,
452
+ beforeRecord: room,
453
+ });
454
+ } catch (error) {
455
+ collector.recordTrace(id, "session.children_failed", { error: error instanceof Error ? error.message : "unknown" });
456
+ }
457
+ };
458
+
406
459
  /**
407
460
  * Everything a settled run records, in the desktop observer's order:
408
461
  * messages, sub-agents, the idle event, the snapshot, the trace and the
@@ -444,23 +497,7 @@ export default function (pi: any) {
444
497
  }
445
498
  pending += bytes;
446
499
  };
447
- try {
448
- await captureChildSessions({
449
- collector,
450
- rootSessionId: id,
451
- parentSessionId: id,
452
- parentEntries: state.ctx?.sessionManager?.getBranch?.() ?? [],
453
- depth: 1,
454
- sessionDirs: sessionDirs(state.ctx),
455
- checkpoints: await collector.childCheckpoints(id),
456
- known: new Set(await collector.childSessionIds(id)),
457
- visited: new Set([id]),
458
- seenFiles: state.seenFiles,
459
- beforeRecord: room,
460
- });
461
- } catch (error) {
462
- collector.recordTrace(id, "session.children_failed", { error: error instanceof Error ? error.message : "unknown" });
463
- }
500
+ await captureChildren(state, room);
464
501
  room(newest.bytes);
465
502
  collector.recordTrace(id, "session.idle", { status });
466
503
  // The turn snapshot runs first so the artifacts it discovers are part of
@@ -485,11 +522,34 @@ export default function (pi: any) {
485
522
  * settled too, so they reach the trace.
486
523
  */
487
524
  const settleRemaining = async (): Promise<void> => {
525
+ // Background sub-agents still running are stopped first (their session
526
+ // files are complete once they exited) and marked interrupted.
527
+ const hook = (globalThis as any)[AGENTS_HOOK] as AgentsHook | undefined;
528
+ const followed = session;
529
+ const interrupted = followed?.started && hook ? await hook.interruptAll(followed.id).catch(() => []) : [];
488
530
  await settleRunning("interrupted");
489
531
  const state = session;
490
532
  if (!state?.started) return;
533
+ for (const child of interrupted) {
534
+ collector.recordTrace(state.id, "session.child_interrupted", { child_session_id: child.sessionId, agent: child.role });
535
+ }
491
536
  const lastId = messagesOf(state).at(-1)?.info.id;
492
- if (typeof lastId !== "string" || lastId === state.lastMessageId) return;
537
+ if (typeof lastId !== "string" || lastId === state.lastMessageId) {
538
+ // No new message of its own: what its sub-agents did since the last settle.
539
+ settling = settling.then(async () => {
540
+ let pending = 0;
541
+ await captureChildren(state, (bytes) => {
542
+ if (pending > 0 && pending + bytes > MAX_TURN_MESSAGE_BYTES) {
543
+ collector.flushTrace(state.id);
544
+ pending = 0;
545
+ }
546
+ pending += bytes;
547
+ });
548
+ collector.flushTrace(state.id);
549
+ }).catch(() => undefined);
550
+ await settling;
551
+ return;
552
+ }
493
553
  settling = settling.then(() => settle(state, "idle")).catch(() => undefined);
494
554
  await settling;
495
555
  };
@@ -591,6 +651,13 @@ export default function (pi: any) {
591
651
  archive.turnFollowed(state.id);
592
652
  });
593
653
 
654
+ // A run that no prompt started (a background sub-agent's results
655
+ // delivered as a message start one): it settles like any other.
656
+ pi.on("agent_start", (_event: any, ctx: any) => {
657
+ const state = follow(ctx, false);
658
+ if (state?.started) state.running = true;
659
+ });
660
+
594
661
  pi.on("tool_execution_start", (event: any) => {
595
662
  if (session && typeof event?.toolCallId === "string") session.toolStarts.set(event.toolCallId, Date.now());
596
663
  });
@@ -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) {
@@ -138,3 +138,132 @@ export function sanitizeGatewayErrorBody({
138
138
  const message = friendlyGatewayMessage(status, ref, attempts, waitedMs);
139
139
  return { bodyText: message, sanitized: true };
140
140
  }
141
+
142
+ // --- stream stall watchdog -------------------------------------------------
143
+ //
144
+ // The agent core runs on Bun, and there its undici transport never
145
+ // settles a pending body read when the connection dies mid-stream: the
146
+ // HTTP idle timeout (or the gateway/edge dropping the socket) closes the
147
+ // socket, but `reader.read()` stays pending forever. The turn then sits
148
+ // on "Working…" until the user presses Esc — no error, no auto-retry
149
+ // (field report: session 01a0dd1a, a 13-minute hang after a tool call).
150
+ // On Node the same close surfaces as a `terminated` error that pi retries.
151
+ // The watchdog gives Bun that behavior: it re-exposes the body through a
152
+ // stream of our own and errors it when the upstream read has been
153
+ // pending for `idleMs`, so pi's retry takes over.
154
+
155
+ /** setTimeout's ceiling; pi passes it (2^31-1) for a disabled timeout. */
156
+ const MAX_TIMER_MS = 2_147_483_647;
157
+ export const DEFAULT_STREAM_IDLE_MS = 300_000;
158
+
159
+ /**
160
+ * The idle limit for a model stream: OMNIRUSH_STREAM_IDLE_MS wins, then
161
+ * the request timeout pi hands the provider (its httpIdleTimeoutMs /
162
+ * retry.provider.timeoutMs), then 5 minutes. 0 (or a value at the timer
163
+ * ceiling, pi's "disabled") turns the watchdog off.
164
+ */
165
+ export function streamIdleMs(envValue: string | undefined, requestTimeoutMs: unknown): number {
166
+ const pick = (value: unknown): number | undefined => {
167
+ if (value === undefined || value === null || value === "") return undefined;
168
+ const n = Number(value);
169
+ if (!Number.isFinite(n) || n < 0) return undefined;
170
+ return n >= MAX_TIMER_MS ? 0 : Math.floor(n);
171
+ };
172
+ return pick(envValue) ?? pick(requestTimeoutMs) ?? DEFAULT_STREAM_IDLE_MS;
173
+ }
174
+
175
+ /** The error a stalled stream fails with ("timed out" keeps it retryable for pi). */
176
+ export function streamStallError(idleMs: number): Error {
177
+ const seconds = Math.max(1, Math.round(idleMs / 1000));
178
+ const error = new Error(
179
+ `Omnirush model stream timed out: no data from the gateway for ${seconds}s — retrying`,
180
+ );
181
+ error.name = "OmnirushStreamStall";
182
+ return error;
183
+ }
184
+
185
+ /**
186
+ * Re-expose `source` as a new ReadableStream that errors with
187
+ * `streamStallError` when one upstream read stays pending for `idleMs`
188
+ * (0 = never), and with the abort reason when `signal` aborts. Every
189
+ * chunk is handed to `onChunk` before it is enqueued. The upstream
190
+ * reader is cancelled (best effort, never awaited — it may be the hung
191
+ * one) whenever the wrapper errors or is cancelled.
192
+ */
193
+ export function withStallWatchdog(
194
+ source: ReadableStream<any>,
195
+ {
196
+ idleMs,
197
+ signal,
198
+ onChunk,
199
+ }: { idleMs: number; signal?: AbortSignal | null; onChunk?: (chunk: any) => void },
200
+ ): ReadableStream<any> {
201
+ const reader = source.getReader();
202
+ let controller: ReadableStreamDefaultController<any>;
203
+ let finished = false;
204
+ let timer: ReturnType<typeof setTimeout> | undefined;
205
+
206
+ const clearTimer = () => {
207
+ if (timer !== undefined) clearTimeout(timer);
208
+ timer = undefined;
209
+ };
210
+ const finish = () => {
211
+ finished = true;
212
+ clearTimer();
213
+ signal?.removeEventListener?.("abort", onAbort);
214
+ };
215
+ const fail = (error: unknown) => {
216
+ if (finished) return;
217
+ finish();
218
+ try {
219
+ controller.error(error);
220
+ } catch {
221
+ /* already closed */
222
+ }
223
+ try {
224
+ reader.cancel(error).catch(() => undefined);
225
+ } catch {
226
+ /* already released */
227
+ }
228
+ };
229
+ function onAbort() {
230
+ const reason = (signal as any)?.reason;
231
+ fail(reason ?? Object.assign(new Error("This operation was aborted"), { name: "AbortError" }));
232
+ }
233
+
234
+ return new ReadableStream<any>({
235
+ start(c) {
236
+ controller = c;
237
+ if (signal?.aborted) onAbort();
238
+ else signal?.addEventListener?.("abort", onAbort, { once: true });
239
+ },
240
+ async pull(c) {
241
+ if (finished) return;
242
+ if (idleMs > 0) timer = setTimeout(() => fail(streamStallError(idleMs)), Math.min(idleMs, MAX_TIMER_MS - 1));
243
+ let result: ReadableStreamReadResult<any>;
244
+ try {
245
+ result = await reader.read();
246
+ } catch (error) {
247
+ fail(error);
248
+ return;
249
+ }
250
+ clearTimer();
251
+ if (finished) return;
252
+ if (result.done) {
253
+ finish();
254
+ c.close();
255
+ return;
256
+ }
257
+ try {
258
+ onChunk?.(result.value);
259
+ } catch {
260
+ /* observers never break the stream */
261
+ }
262
+ c.enqueue(result.value);
263
+ },
264
+ cancel(reason) {
265
+ finish();
266
+ return reader.cancel(reason).catch(() => undefined);
267
+ },
268
+ });
269
+ }
@@ -14,6 +14,9 @@
14
14
  // - on 429 / 5xx / network failures, retries with exponential backoff
15
15
  // + full jitter (shared retry layer, "attempt 2/5" progress on
16
16
  // stderr) so users ride out backend deploys.
17
+ // - fails a model stream that stops delivering data (stall watchdog)
18
+ // with a retryable error, so a dropped connection never leaves the
19
+ // turn hanging on Bun (pi retries it like on Node).
17
20
  // - SANITIZES every non-OK response body: raw upstream provider text
18
21
  // ("help.openai.com… request ID…") never reaches the user — the
19
22
  // agent layer gets an omnirush message with a local correlation id
@@ -28,12 +31,15 @@ import fs from "node:fs";
28
31
  import { openAIResponsesApi } from "@earendil-works/pi-ai";
29
32
  import { recordGatewayUsage } from "./usage";
30
33
  import { sharedRefresher } from "./refresh";
34
+ import { sendWithDeviceAuth, sessionEndedMessage } from "./auth";
31
35
  import {
32
36
  createSseDataScanner,
33
37
  formatSotaWarning,
34
38
  isSotaViolation,
35
39
  sanitizeGatewayErrorBody,
36
40
  servedModelFromSseData,
41
+ streamIdleMs,
42
+ withStallWatchdog,
37
43
  } from "./sota-lib";
38
44
  import {
39
45
  formatUnavailableSummary,
@@ -197,8 +203,9 @@ async function readErrorBody(response: any): Promise<string> {
197
203
 
198
204
  /**
199
205
  * Wrap a fetch implementation so that:
200
- * - 401 responses trigger one single-flight device-token refresh and a
201
- * 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),
202
209
  * - 429 / 5xx / network failures retry with exponential backoff + full
203
210
  * jitter (progress lines on stderr), and
204
211
  * - SSE response bodies stream through a pass-through tap that observes
@@ -209,12 +216,15 @@ async function readErrorBody(response: any): Promise<string> {
209
216
  * Retries only cover the request up to (and including) the response
210
217
  * headers — once an OK response body starts streaming it is forwarded
211
218
  * untouched; a mid-stream failure surfaces as the provider's own error.
219
+ * A body that stops delivering data for `idleMs` fails with a retryable
220
+ * "timed out" error (stall watchdog, see sota-lib) instead of hanging.
212
221
  * Client aborts (Esc) are never retried.
213
222
  */
214
223
  function tapFetch(
215
224
  baseFetch: any,
216
225
  expectedModelId: string,
217
226
  onViolation: (expected: string, served: string) => void,
227
+ idleMs: number,
218
228
  ): any {
219
229
  return async (input: any, init: any) => {
220
230
  const requestUrl = typeof input === "string" ? input : input?.url;
@@ -223,47 +233,46 @@ function tapFetch(
223
233
 
224
234
  const outcome = await withRetries(
225
235
  async (attempt, ref) => {
226
- let response = await doFetch();
227
- debug(`provider fetch ${requestUrl} -> ${response?.status} (attempt ${attempt})`);
228
- // Capture the gateway's grant/usage headers (x-omnirush-* plus
229
- // the x-ratelimit-*-tokens grant pair) straight from the raw
230
- // response — guaranteed availability here, independent of pi's
231
- // event plumbing.
232
- try {
233
- recordGatewayUsage(response?.headers);
234
- } catch {
235
- /* usage capture must never break the request */
236
- }
237
- if (response?.status === 401) {
238
- const tokenUsed = bearerFromInit(init) || bearerFromInit(input);
239
- debug(`401 seen; bearer present: ${Boolean(tokenUsed)}`);
240
- if (tokenUsed) {
241
- const refresher = sharedRefresher();
242
- const refreshed = await refresher.refresh(tokenUsed).catch((error: any) => {
243
- debug(`refresh threw: ${error?.message ?? error}`);
244
- return false;
245
- });
246
- const next = refresher.auth?.accessToken;
247
- debug(`refresh ok: ${refreshed}, next token present: ${Boolean(next)}`);
248
- if (refreshed && next) {
249
- response = await doFetch(next);
250
- debug(`retry-after-refresh status: ${response?.status}`);
251
- try {
252
- recordGatewayUsage(response?.headers);
253
- } catch {
254
- /* as above */
255
- }
256
- }
257
- if (response?.status === 401) {
258
- // The refresh (or the fresh-adopted token) was rejected:
259
- // the session is dead. Retrying forever would spin pi's
260
- // auto-retry loop silently — fail fast with the fix.
261
- warnStderr(
262
- "Omnirush: this device's session is no longer valid — run `omnirush login` to sign in again.",
263
- );
264
- process.exit(2);
265
- }
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 */
266
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
+ );
267
276
  }
268
277
  const retryAfterSec = Number(response?.headers?.get?.("retry-after")) || undefined;
269
278
  return { response, ref, retryAfterSec };
@@ -353,13 +362,11 @@ function tapFetch(
353
362
  };
354
363
  const scanner = createSseDataScanner(onData);
355
364
  const decoder = new TextDecoder();
356
- const passthrough = new TransformStream({
357
- transform(chunk: any, controller: any) {
358
- controller.enqueue(chunk);
359
- scanner(decoder.decode(chunk, { stream: true }));
360
- },
365
+ const body = withStallWatchdog(response.body, {
366
+ idleMs,
367
+ signal: init?.signal,
368
+ onChunk: (chunk: any) => scanner(decoder.decode(chunk, { stream: true })),
361
369
  });
362
- const body = response.body.pipeThrough(passthrough);
363
370
  return new Response(body, {
364
371
  status: response.status,
365
372
  statusText: response.statusText,
@@ -389,8 +396,11 @@ export default function (pi: any) {
389
396
  const baseFetch = options?.fetch ?? globalThis.fetch;
390
397
  const tapped = {
391
398
  ...(options ?? {}),
392
- fetch: tapFetch(baseFetch, model.id, (expected, served) =>
393
- reportViolation(strictSota(), expected, served),
399
+ fetch: tapFetch(
400
+ baseFetch,
401
+ model.id,
402
+ (expected, served) => reportViolation(strictSota(), expected, served),
403
+ streamIdleMs(process.env.OMNIRUSH_STREAM_IDLE_MS, options?.timeoutMs),
394
404
  ),
395
405
  };
396
406
  return simple
@@ -406,7 +416,15 @@ export default function (pi: any) {
406
416
  apiKey: {
407
417
  name: "Omnirush token",
408
418
  async resolve() {
409
- 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();
410
428
  if (!key) return undefined;
411
429
  return { auth: { apiKey: key }, source: "environment" };
412
430
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnirush",
3
- "version": "0.8.4",
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",