omnirush 0.8.5 → 0.9.0

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 (49) hide show
  1. package/assets/CHANGELOG.md +85 -0
  2. package/assets/collect-once.ts +6 -5
  3. package/assets/extensions/omnirush/agents-lib.ts +196 -12
  4. package/assets/extensions/omnirush/agents.ts +97 -105
  5. package/assets/extensions/omnirush/auth.js +399 -115
  6. package/assets/extensions/omnirush/bgshell-lib.ts +400 -0
  7. package/assets/extensions/omnirush/bgshell.ts +392 -0
  8. package/assets/extensions/omnirush/collector.ts +103 -16
  9. package/assets/extensions/omnirush/commands.ts +14 -15
  10. package/assets/extensions/omnirush/deliveries.ts +145 -0
  11. package/assets/extensions/omnirush/guard/UPSTREAM +2 -0
  12. package/assets/extensions/omnirush/guard/git-command-policy.ts +885 -0
  13. package/assets/extensions/omnirush/guard-lib.ts +230 -0
  14. package/assets/extensions/omnirush/guard.ts +340 -0
  15. package/assets/extensions/omnirush/index.ts +12 -0
  16. package/assets/extensions/omnirush/pi-engine.ts +65 -1
  17. package/assets/extensions/omnirush/sota.ts +104 -47
  18. package/assets/extensions/omnirush/status-lib.ts +3 -0
  19. package/assets/extensions/omnirush/subagents-lib.ts +623 -0
  20. package/assets/extensions/omnirush/subagents.ts +305 -0
  21. package/assets/extensions/omnirush/swarm-lib.ts +142 -0
  22. package/assets/extensions/omnirush/swarm.ts +95 -0
  23. package/assets/extensions/omnirush/voice/capture.ts +502 -0
  24. package/assets/extensions/omnirush/voice/core/UPSTREAM +16 -0
  25. package/assets/extensions/omnirush/voice/core/file-source.ts +70 -0
  26. package/assets/extensions/omnirush/voice/core/index.ts +21 -0
  27. package/assets/extensions/omnirush/voice/core/keyterms.ts +117 -0
  28. package/assets/extensions/omnirush/voice/core/resample.ts +63 -0
  29. package/assets/extensions/omnirush/voice/core/segmenter.ts +231 -0
  30. package/assets/extensions/omnirush/voice/core/session.ts +403 -0
  31. package/assets/extensions/omnirush/voice/core/text.ts +81 -0
  32. package/assets/extensions/omnirush/voice/core/transcriber.ts +135 -0
  33. package/assets/extensions/omnirush/voice/core/types.ts +102 -0
  34. package/assets/extensions/omnirush/voice/core/wav.ts +95 -0
  35. package/assets/extensions/omnirush/voice/keys.ts +435 -0
  36. package/assets/extensions/omnirush/voice/kitty.ts +64 -0
  37. package/assets/extensions/omnirush/voice/pvrecorder-worker.cjs +43 -0
  38. package/assets/extensions/omnirush/voice/settings.ts +67 -0
  39. package/assets/extensions/omnirush/voice.ts +838 -0
  40. package/assets/extensions/omnirush/yolo-lib.ts +80 -0
  41. package/assets/extensions/omnirush/yolo.ts +85 -0
  42. package/package.json +6 -2
  43. package/scripts/brand-engine.js +526 -0
  44. package/scripts/build-all-packages.py +29 -1
  45. package/scripts/smoke-packages.py +32 -1
  46. package/src/bin.js +252 -53
  47. package/src/compat.js +272 -0
  48. package/src/lib.js +64 -0
  49. package/scripts/patch-pi-branding.js +0 -251
@@ -33,6 +33,10 @@
33
33
  // a command the user ran)
34
34
  // custom / branch summary -> user message with synthetic text parts (what
35
35
  // the model was shown; never a turn opener)
36
+ // background command -> the delivery message as above, answered by an
37
+ // results (bgshell.ts) assistant message holding one bash tool part
38
+ // per finished command (its output and exit
39
+ // code; no model step, no tokens)
36
40
  // compaction -> user message with a compaction part and the
37
41
  // summary as an assistant `summary` message
38
42
  // system prompt -> not a message (the desktop records it in
@@ -49,6 +53,9 @@
49
53
  import { createHash } from "node:crypto";
50
54
  import { isAbsolute, resolve } from "node:path";
51
55
 
56
+ /** customType of bgshell.ts's finished-background-command message. */
57
+ export const BACKGROUND_BASH_TYPE = "omnirush-bash-result";
58
+
52
59
  export type EngineMessage = { info: Record<string, unknown>; parts: Array<Record<string, unknown>> };
53
60
 
54
61
  /** One pi session entry (loosely typed: the files are parsed without validation). */
@@ -295,6 +302,60 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
295
302
  return messageVariant ?? null;
296
303
  };
297
304
 
305
+ /**
306
+ * Finished background shell commands (bgshell.ts delivery): their output
307
+ * as bash tool parts, the way the engine records a command the user ran.
308
+ */
309
+ const backgroundBash = (parentId: string, entryId: string, details: unknown, at: number | null, index: number): void => {
310
+ const jobs = isRecord(details) && Array.isArray(details.jobs) ? details.jobs.filter(isRecord) : [];
311
+ if (jobs.length === 0) return;
312
+ const shellId = engineMessageId(entryId, "_bash");
313
+ const shellModel = model ?? nextAssistantModel.get(index) ?? null;
314
+ const time = at ?? 0;
315
+ const parts: Array<Record<string, unknown>> = jobs.map((job, jobIndex) => {
316
+ const start = typeof job.started_at === "number" ? job.started_at : time;
317
+ const end = typeof job.finished_at === "number" ? job.finished_at : time;
318
+ const failed = job.status === "failed";
319
+ return {
320
+ type: "tool",
321
+ callID: `bg_${typeof job.id === "string" ? job.id : jobIndex}_${entryId}`,
322
+ tool: "bash",
323
+ state: {
324
+ status: failed ? "error" : "completed",
325
+ input: { command: typeof job.command === "string" ? job.command : "", background_id: job.id ?? null },
326
+ ...(failed ? { error: typeof job.error === "string" ? job.error : "failed" } : {}),
327
+ output: typeof job.output === "string" ? job.output : "",
328
+ metadata: {
329
+ background: true,
330
+ ...(typeof job.exit_code === "number" ? { exit: job.exit_code } : {}),
331
+ ...(typeof job.status === "string" ? { status: job.status } : {}),
332
+ ...(job.interrupted === true ? { interrupted: true } : {}),
333
+ ...(job.truncated === true ? { truncated: true } : {}),
334
+ ...(typeof job.tool_call_id === "string" ? { startedBy: job.tool_call_id } : {}),
335
+ },
336
+ time: { start, end },
337
+ },
338
+ };
339
+ });
340
+ out.push({
341
+ info: {
342
+ id: shellId,
343
+ sessionID: sessionId,
344
+ role: "assistant",
345
+ parentID: parentId,
346
+ time: { created: time, completed: time },
347
+ ...(shellModel ? { modelID: shellModel.modelID, providerID: shellModel.providerID } : {}),
348
+ agent,
349
+ mode: agent,
350
+ path: { cwd, root: cwd },
351
+ cost: 0,
352
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
353
+ finish: "stop",
354
+ },
355
+ parts: parts.map((part, partIndex) => ({ id: partId(shellId, partIndex), sessionID: sessionId, messageID: shellId, ...part })),
356
+ });
357
+ };
358
+
298
359
  entries.forEach((entry, index) => {
299
360
  const entryId = typeof entry.id === "string" && entry.id ? entry.id : null;
300
361
  if (!entryId) return;
@@ -340,7 +401,9 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
340
401
  }
341
402
  if (entry.type === "custom_message") {
342
403
  const parts = contentParts(entry.content, { synthetic: true, metadata: { customType: entry.customType ?? null } });
343
- if (parts.length > 0) userMessage(engineMessageId(entryId), at, parts, index);
404
+ const id = engineMessageId(entryId);
405
+ if (parts.length > 0) userMessage(id, at, parts, index);
406
+ if (entry.customType === BACKGROUND_BASH_TYPE) backgroundBash(id, entryId, (entry as any).details, at, index);
344
407
  return;
345
408
  }
346
409
  if (entry.type !== "message" || !isRecord(entry.message)) return;
@@ -356,6 +419,7 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
356
419
  case "custom": {
357
420
  const parts = contentParts(message.content, { synthetic: true, metadata: { customType: message.customType ?? null } });
358
421
  if (parts.length > 0) userMessage(id, created, parts, index);
422
+ if (message.customType === BACKGROUND_BASH_TYPE) backgroundBash(id, entryId, message.details, created, index);
359
423
  return;
360
424
  }
361
425
  case "branchSummary":
@@ -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,
@@ -40,6 +41,14 @@ import {
40
41
  streamIdleMs,
41
42
  withStallWatchdog,
42
43
  } from "./sota-lib";
44
+ import { omniDir } from "./auth";
45
+ import {
46
+ ENV_FALLBACK_EFFORT,
47
+ ENV_FALLBACK_MODEL,
48
+ FALLBACK_MARKER,
49
+ subagentFallbackFetch,
50
+ type FallbackRoute,
51
+ } from "./subagents-lib";
43
52
  import {
44
53
  formatUnavailableSummary,
45
54
  isRetryableStatus,
@@ -202,8 +211,9 @@ async function readErrorBody(response: any): Promise<string> {
202
211
 
203
212
  /**
204
213
  * 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),
214
+ * - every attempt carries the current on-disk device token, and a 401
215
+ * adopts a newer on-disk pair or refreshes under the cross-process
216
+ * lock, then resends (auth.js sendWithDeviceAuth),
207
217
  * - 429 / 5xx / network failures retry with exponential backoff + full
208
218
  * jitter (progress lines on stderr), and
209
219
  * - SSE response bodies stream through a pass-through tap that observes
@@ -220,7 +230,7 @@ async function readErrorBody(response: any): Promise<string> {
220
230
  */
221
231
  function tapFetch(
222
232
  baseFetch: any,
223
- expectedModelId: string,
233
+ expectedModel: string | (() => string),
224
234
  onViolation: (expected: string, served: string) => void,
225
235
  idleMs: number,
226
236
  ): any {
@@ -231,47 +241,46 @@ function tapFetch(
231
241
 
232
242
  const outcome = await withRetries(
233
243
  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
- }
244
+ // Every attempt carries the CURRENT on-disk token (re-read per
245
+ // request: another terminal or a sub-agent may have rotated the
246
+ // pair since this process started). A 401 adopts a newer on-disk
247
+ // pair or refreshes under the cross-process lock, then resends.
248
+ const recordUsage = (res: any) => {
249
+ try {
250
+ recordGatewayUsage(res?.headers);
251
+ } catch {
252
+ /* usage capture must never break the request */
274
253
  }
254
+ };
255
+ const { response: answered, outcome: authOutcome } = await sendWithDeviceAuth(
256
+ (token: string) => doFetch(token || undefined),
257
+ sharedRefresher(),
258
+ {
259
+ fallbackToken: bearerFromInit(init) || bearerFromInit(input),
260
+ onResponse: (res: any) => {
261
+ debug(`provider fetch ${requestUrl} -> ${res?.status} (attempt ${attempt})`);
262
+ recordUsage(res);
263
+ },
264
+ },
265
+ );
266
+ let response = answered;
267
+ if (authOutcome?.status === "dead") {
268
+ // The manager refused the refresh token that is on disk right
269
+ // now (re-read under the refresh lock): the device session is
270
+ // gone (revoked, expired, signed out). Retrying would spin pi's
271
+ // auto-retry loop silently — fail fast with the fix.
272
+ debug(`device session ended: ${authOutcome.reason} ${authOutcome.detail ?? ""}`);
273
+ warnStderr(sessionEndedMessage(authOutcome));
274
+ process.exit(2);
275
+ }
276
+ if (authOutcome?.status === "transient") {
277
+ // Could not reach the manager to refresh: not a sign-out. Retry
278
+ // like any other brief outage.
279
+ debug(`refresh unavailable: ${authOutcome.error}`);
280
+ response = new Response(
281
+ JSON.stringify({ error: { message: "Omnirush sign-in service unavailable", type: "omnirush_error", code: "auth_refresh_unavailable" } }),
282
+ { status: 503, headers: { "content-type": "application/json" } },
283
+ );
275
284
  }
276
285
  const retryAfterSec = Number(response?.headers?.get?.("retry-after")) || undefined;
277
286
  return { response, ref, retryAfterSec };
@@ -355,6 +364,8 @@ function tapFetch(
355
364
  const onData = (data: string) => {
356
365
  if (reported) return;
357
366
  const served = servedModelFromSseData(data);
367
+ // A sub-agent moved to the main model expects that one.
368
+ const expectedModelId = typeof expectedModel === "function" ? expectedModel() : expectedModel;
358
369
  if (served === undefined || !isSotaViolation(expectedModelId, served)) return;
359
370
  reported = true;
360
371
  onViolation(expectedModelId, served);
@@ -389,22 +400,60 @@ export default function (pi: any) {
389
400
  const responses = openAIResponsesApi();
390
401
  const strictSota = (): boolean => pi.getFlag("strict-sota") === true;
391
402
 
403
+ // A sub-agent on a picked model: the main model it falls back to when the
404
+ // gateway refuses the picked one (set by the parent's spawn_agents).
405
+ const fallbackModel = (process.env[ENV_FALLBACK_MODEL] || "").trim();
406
+ const fallbackEffort = (process.env[ENV_FALLBACK_EFFORT] || "").trim() || null;
407
+
392
408
  const streamWith =
393
409
  (simple: boolean) =>
394
410
  (model: any, context: any, options: any) => {
395
- const baseFetch = options?.fetch ?? globalThis.fetch;
411
+ let baseFetch = options?.fetch ?? globalThis.fetch;
412
+ const route: FallbackRoute = { expected: model.id, movedTo: null };
413
+ if (fallbackModel && fallbackModel !== model.id) {
414
+ baseFetch = subagentFallbackFetch(baseFetch, {
415
+ model: fallbackModel,
416
+ effort: fallbackEffort,
417
+ dir: omniDir(),
418
+ route,
419
+ report: (event) => {
420
+ // The parent's spawn_agents reads this line from the child's stderr.
421
+ try {
422
+ process.stderr.write(`\n${FALLBACK_MARKER}${JSON.stringify(event)}\n`);
423
+ } catch {
424
+ /* stderr may be closed */
425
+ }
426
+ },
427
+ });
428
+ }
396
429
  const tapped = {
397
430
  ...(options ?? {}),
398
431
  fetch: tapFetch(
399
432
  baseFetch,
400
- model.id,
433
+ () => route.expected,
401
434
  (expected, served) => reportViolation(strictSota(), expected, served),
402
435
  streamIdleMs(process.env.OMNIRUSH_STREAM_IDLE_MS, options?.timeoutMs),
403
436
  ),
404
437
  };
405
- return simple
438
+ const stream = simple
406
439
  ? responses.streamSimple(model, context, tapped)
407
440
  : responses.stream(model, context, tapped);
441
+ if (fallbackModel && stream && typeof stream.push === "function") {
442
+ // The message records the model (and effort) that really answered.
443
+ const push = stream.push.bind(stream);
444
+ stream.push = (event: any) => {
445
+ if (route.movedTo) {
446
+ for (const message of [event?.partial, event?.message, event?.error]) {
447
+ if (message && typeof message === "object" && message.role === "assistant") {
448
+ message.model = route.movedTo.model;
449
+ if (route.movedTo.effort) message.providerThinkingLevel = route.movedTo.effort;
450
+ }
451
+ }
452
+ }
453
+ return push(event);
454
+ };
455
+ }
456
+ return stream;
408
457
  };
409
458
 
410
459
  pi.registerProvider({
@@ -415,7 +464,15 @@ export default function (pi: any) {
415
464
  apiKey: {
416
465
  name: "Omnirush token",
417
466
  async resolve() {
418
- const key = process.env.OMNIRUSH_TOKEN;
467
+ // The shared auth file first (always current); the launcher's
468
+ // environment value only as a bootstrap.
469
+ let key = "";
470
+ try {
471
+ key = sharedRefresher().accessToken();
472
+ } catch {
473
+ /* fall back to the environment */
474
+ }
475
+ key ||= (process.env.OMNIRUSH_TOKEN || "").trim();
419
476
  if (!key) return undefined;
420
477
  return { auth: { apiKey: key }, source: "environment" };
421
478
  },
@@ -52,6 +52,8 @@ export interface StatusInput {
52
52
  origin: string;
53
53
  /** omnirush CLI version ("unknown" when not provided by the launcher). */
54
54
  version: string;
55
+ /** The approval mode line (yolo-lib formatModeStatus), when known. */
56
+ yolo?: string | null;
55
57
  }
56
58
 
57
59
  /**
@@ -79,6 +81,7 @@ export function formatStatusLines(input: StatusInput): string[] {
79
81
  }
80
82
 
81
83
  lines.push(formatCollectorLine(input.collector));
84
+ if (input.yolo) lines.push(input.yolo);
82
85
  lines.push(`manager: ${input.origin}`);
83
86
  lines.push(`version: ${input.version}`);
84
87
  return lines;