omnirush 0.9.1 → 0.10.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.
- package/assets/extensions/omnirush/agents-lib.ts +53 -15
- package/assets/extensions/omnirush/bgshell.ts +9 -1
- package/assets/extensions/omnirush/capture/session-archive/index.ts +5 -12
- package/assets/extensions/omnirush/capture/session-archive/touched.ts +2 -7
- package/assets/extensions/omnirush/capture/workspace-collector.ts +1 -2
- package/assets/extensions/omnirush/mcp.ts +2 -1
- package/assets/extensions/omnirush/secret-env.ts +15 -0
- package/assets/extensions/omnirush/subagents-lib.ts +5 -0
- package/package.json +2 -2
- package/scripts/__pycache__/build-all-packages.cpython-38.pyc +0 -0
- package/scripts/__pycache__/smoke-packages.cpython-38.pyc +0 -0
- package/src/bin.js +102 -22
- package/src/lib.js +7 -2
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type SubagentFallback,
|
|
40
40
|
} from "./subagents-lib";
|
|
41
41
|
import { childAuthEnv } from "./auth";
|
|
42
|
+
import { sanitizeToolEnvironment } from "./secret-env";
|
|
42
43
|
import { yoloActive } from "./yolo-lib";
|
|
43
44
|
|
|
44
45
|
/** childAuthEnv, never throwing (a spawn must not fail on the auth file). */
|
|
@@ -444,6 +445,8 @@ const ROLE_RE = /^\s*\{\s*"type"\s*:\s*"message_end"\s*,\s*"message"\s*:\s*\{\s*
|
|
|
444
445
|
export class ChildEventScanner {
|
|
445
446
|
finalText = "";
|
|
446
447
|
turns = 0;
|
|
448
|
+
/** Monotonic count of model/tool events that represent real progress. */
|
|
449
|
+
progressSerial = 0;
|
|
447
450
|
readonly toolsRunning = new Set<string>();
|
|
448
451
|
private line = "";
|
|
449
452
|
/** head: deciding from the first characters; keep: buffering the whole line; skip: dropping it. */
|
|
@@ -486,6 +489,7 @@ export class ChildEventScanner {
|
|
|
486
489
|
const head = this.line.slice(0, CHILD_EVENT_HEAD_CHARS);
|
|
487
490
|
const type = EVENT_TYPE_RE.exec(head)?.[1] ?? null;
|
|
488
491
|
this.lineType = type;
|
|
492
|
+
if (type === "message_update" || type === "message_start" || type === "turn_start") this.progressSerial += 1;
|
|
489
493
|
if (type === "message_end") {
|
|
490
494
|
const role = ROLE_RE.exec(head)?.[1] ?? null;
|
|
491
495
|
this.lineRole = role;
|
|
@@ -514,6 +518,7 @@ export class ChildEventScanner {
|
|
|
514
518
|
}
|
|
515
519
|
|
|
516
520
|
private tool(type: string, id: string): void {
|
|
521
|
+
this.progressSerial += 1;
|
|
517
522
|
if (type === "tool_execution_start") this.toolsRunning.add(id);
|
|
518
523
|
else this.toolsRunning.delete(id);
|
|
519
524
|
}
|
|
@@ -530,7 +535,7 @@ export class ChildEventScanner {
|
|
|
530
535
|
if (!trimmed || !trimmed.includes('"type"')) return;
|
|
531
536
|
// A short line: skip the types nothing is read from without parsing them.
|
|
532
537
|
const type = mode === "head" ? EVENT_TYPE_RE.exec(trimmed)?.[1] : undefined;
|
|
533
|
-
if (type && type !== "message_end" && type !== "tool_execution_start" && type !== "tool_execution_end") return;
|
|
538
|
+
if (type && type !== "message_end" && type !== "message_update" && type !== "message_start" && type !== "turn_start" && type !== "tool_execution_start" && type !== "tool_execution_end") return;
|
|
534
539
|
let event: any;
|
|
535
540
|
try {
|
|
536
541
|
event = JSON.parse(trimmed);
|
|
@@ -541,10 +546,15 @@ export class ChildEventScanner {
|
|
|
541
546
|
this.tool(event.type, event.toolCallId);
|
|
542
547
|
return;
|
|
543
548
|
}
|
|
549
|
+
if (event?.type === "message_update" || event?.type === "message_start" || event?.type === "turn_start") {
|
|
550
|
+
this.progressSerial += 1;
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
544
553
|
if (event?.type !== "message_end" || !event.message) return;
|
|
545
554
|
const message = event.message;
|
|
546
555
|
if (message.role !== "assistant") return;
|
|
547
556
|
this.turns += 1;
|
|
557
|
+
this.progressSerial += 1;
|
|
548
558
|
const parts = Array.isArray(message.content) ? message.content : [];
|
|
549
559
|
const text = parts
|
|
550
560
|
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
|
|
@@ -602,7 +612,7 @@ export interface SpawnChildOptions {
|
|
|
602
612
|
now?: () => number;
|
|
603
613
|
/** Progress callback: partial output lines as the child runs. */
|
|
604
614
|
onChildStdout?: (role: AgentRole, chunk: string) => void;
|
|
605
|
-
/**
|
|
615
|
+
/** Meaningful model/tool progress from the child, with its event types. */
|
|
606
616
|
onActivity?: (activity: ChildActivity) => void;
|
|
607
617
|
/** The child's session id (a fresh UUID by default). */
|
|
608
618
|
sessionId?: string;
|
|
@@ -618,6 +628,7 @@ export interface ChildActivity {
|
|
|
618
628
|
turns: number;
|
|
619
629
|
/** Tool calls running right now. */
|
|
620
630
|
toolsRunning: number;
|
|
631
|
+
meaningful: true;
|
|
621
632
|
}
|
|
622
633
|
|
|
623
634
|
/**
|
|
@@ -677,6 +688,7 @@ export async function runChildAgent(
|
|
|
677
688
|
const child = spawnImpl(invocation.command, invocation.args, {
|
|
678
689
|
cwd: options.cwd,
|
|
679
690
|
shell: false,
|
|
691
|
+
detached: process.platform !== "win32",
|
|
680
692
|
stdio: ["ignore", "pipe", "pipe"],
|
|
681
693
|
// Shared credentials by location (OMNIRUSH_DIR), never a token
|
|
682
694
|
// frozen at the parent's launch: the child re-reads auth.json for
|
|
@@ -697,16 +709,33 @@ export async function runChildAgent(
|
|
|
697
709
|
};
|
|
698
710
|
|
|
699
711
|
const killTree = () => {
|
|
700
|
-
|
|
701
|
-
child.
|
|
702
|
-
|
|
703
|
-
|
|
712
|
+
const signalGroup = (signal: NodeJS.Signals) => {
|
|
713
|
+
if (process.platform !== "win32" && typeof child.pid === "number" && child.pid > 0) {
|
|
714
|
+
try {
|
|
715
|
+
process.kill(-child.pid, signal);
|
|
716
|
+
return true;
|
|
717
|
+
} catch {
|
|
718
|
+
/* fall back to the direct child below */
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (process.platform === "win32" && typeof child.pid === "number" && child.pid > 0) {
|
|
722
|
+
try {
|
|
723
|
+
const args = ["/PID", String(child.pid), "/T"];
|
|
724
|
+
if (signal === "SIGKILL") args.push("/F");
|
|
725
|
+
nodeSpawn("taskkill", args, { windowsHide: true, stdio: "ignore" });
|
|
726
|
+
return true;
|
|
727
|
+
} catch {
|
|
728
|
+
/* fall back to the direct child below */
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return false;
|
|
732
|
+
};
|
|
733
|
+
if (!signalGroup("SIGTERM")) {
|
|
734
|
+
try { child.kill("SIGTERM"); } catch { /* already gone */ }
|
|
704
735
|
}
|
|
705
736
|
setTimeout(() => {
|
|
706
|
-
|
|
707
|
-
child.kill("SIGKILL");
|
|
708
|
-
} catch {
|
|
709
|
-
/* already gone */
|
|
737
|
+
if (!signalGroup("SIGKILL")) {
|
|
738
|
+
try { child.kill("SIGKILL"); } catch { /* already gone */ }
|
|
710
739
|
}
|
|
711
740
|
}, Math.max(50, options.killGraceMs ?? CHILD_KILL_GRACE_MS)).unref?.();
|
|
712
741
|
};
|
|
@@ -726,10 +755,11 @@ export async function runChildAgent(
|
|
|
726
755
|
watchdog = setTimeout(() => kill("stalled"), window);
|
|
727
756
|
watchdog.unref?.();
|
|
728
757
|
};
|
|
729
|
-
const alive = () => {
|
|
758
|
+
const alive = (meaningful: boolean) => {
|
|
759
|
+
if (!meaningful) return;
|
|
730
760
|
lastActivity = now();
|
|
731
761
|
armWatchdog();
|
|
732
|
-
options.onActivity?.({ at: lastActivity, turns: events.turns, toolsRunning: toolsRunning.size });
|
|
762
|
+
options.onActivity?.({ at: lastActivity, turns: events.turns, toolsRunning: toolsRunning.size, meaningful: true });
|
|
733
763
|
};
|
|
734
764
|
armWatchdog();
|
|
735
765
|
|
|
@@ -749,8 +779,9 @@ export async function runChildAgent(
|
|
|
749
779
|
child.stdout?.setEncoding?.("utf8");
|
|
750
780
|
child.stdout?.on("data", (chunk: Buffer | string) => {
|
|
751
781
|
const text = String(chunk);
|
|
782
|
+
const before = events.progressSerial;
|
|
752
783
|
events.push(text);
|
|
753
|
-
alive();
|
|
784
|
+
alive(events.progressSerial !== before);
|
|
754
785
|
options.onChildStdout?.(task.role, text);
|
|
755
786
|
});
|
|
756
787
|
child.stderr?.setEncoding?.("utf8");
|
|
@@ -760,10 +791,17 @@ export async function runChildAgent(
|
|
|
760
791
|
const lines = (pendingErrLine + text).split("\n");
|
|
761
792
|
pendingErrLine = lines.pop() ?? "";
|
|
762
793
|
if (pendingErrLine.length > 64 * 1024) pendingErrLine = pendingErrLine.slice(-64 * 1024);
|
|
763
|
-
|
|
794
|
+
let meaningful = false;
|
|
795
|
+
for (const line of lines) {
|
|
796
|
+
const fallback = parseFallbackMarker(line);
|
|
797
|
+
if (fallback) {
|
|
798
|
+
gatewayFallback = fallback;
|
|
799
|
+
meaningful = true;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
764
802
|
stderr += text;
|
|
765
803
|
if (stderr.length > 256 * 1024) stderr = stderr.slice(-64 * 1024);
|
|
766
|
-
alive();
|
|
804
|
+
alive(meaningful);
|
|
767
805
|
});
|
|
768
806
|
child.on("error", (error: Error) => {
|
|
769
807
|
finish({
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
} from "./bgshell-lib";
|
|
43
43
|
import { deliveryHub } from "./deliveries";
|
|
44
44
|
import { BACKGROUND_BASH_TYPE } from "./pi-engine";
|
|
45
|
+
import { sanitizeToolEnvironment } from "./secret-env";
|
|
45
46
|
|
|
46
47
|
/** customType of the message that brings finished background commands back (the trace maps it to bash tool parts). */
|
|
47
48
|
export const BASH_DELIVERY_TYPE = BACKGROUND_BASH_TYPE;
|
|
@@ -221,7 +222,14 @@ export default function (pi: any, options: { exec?: ShellExec; now?: () => numbe
|
|
|
221
222
|
const operations = {
|
|
222
223
|
exec: (command: string, cwd: string, execOptions: any) =>
|
|
223
224
|
new Promise<{ exitCode: number | null }>((resolve, reject) => {
|
|
224
|
-
const current = manager.start({
|
|
225
|
+
const current = manager.start({
|
|
226
|
+
session,
|
|
227
|
+
command,
|
|
228
|
+
cwd,
|
|
229
|
+
env: sanitizeToolEnvironment(execOptions?.env ?? process.env),
|
|
230
|
+
toolCallId,
|
|
231
|
+
onData: execOptions?.onData,
|
|
232
|
+
});
|
|
225
233
|
job = current;
|
|
226
234
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
227
235
|
const onAbort = () => { void manager.kill([current]); };
|
|
@@ -79,10 +79,6 @@ const MAX_JOB_AGE_MS = 7 * 24 * 60 * 60_000;
|
|
|
79
79
|
const JOB_RETRY_BASE_MS = 60_000;
|
|
80
80
|
const JOB_RETRY_MAX_MS = 60 * 60_000;
|
|
81
81
|
const SIGN_OUT_ABORT_TIMEOUT_MS = 5_000;
|
|
82
|
-
/** At app start, sessions active (a base, a prompt, a turn) within this long may get a final archive (startFinalCandidates). */
|
|
83
|
-
const START_FINAL_WINDOW_MS = 7 * 24 * 60 * 60_000;
|
|
84
|
-
/** At most this many sessions are scanned for a final archive at app start. */
|
|
85
|
-
const MAX_START_FINALS = 10;
|
|
86
82
|
/**
|
|
87
83
|
* How long a folder policy answer (4.4: all folders, touched files) is
|
|
88
84
|
* reused for folders without `.git`: sessions started meanwhile send no
|
|
@@ -515,12 +511,9 @@ export class SessionArchiver {
|
|
|
515
511
|
* with a turn captured since its last final archive (the app quit before
|
|
516
512
|
* that final, or crashed), the most recently active session on each
|
|
517
513
|
* other folder (the folder may have changed while the app was closed),
|
|
518
|
-
* and every touched-files session (its files are its own).
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
* archive never extends the window, so a chat left alone gets none a week
|
|
522
|
-
* after its last turn, however often the app starts. The most recently
|
|
523
|
-
* active first, at most 10.
|
|
514
|
+
* and every touched-files session (its files are its own). Sessions are
|
|
515
|
+
* recovered until their final archive is settled; there is no age or count
|
|
516
|
+
* cutoff that can silently lose the final workspace state.
|
|
524
517
|
*/
|
|
525
518
|
async startFinalCandidates(): Promise<string[]> {
|
|
526
519
|
try {
|
|
@@ -533,7 +526,7 @@ export class SessionArchiver {
|
|
|
533
526
|
const parsed = sessionStateSchema.safeParse(await readJsonFile(join(this.dirs.sessions, name)));
|
|
534
527
|
if (!parsed.success || name !== `${stateKey(parsed.data.session_id)}.json`) continue;
|
|
535
528
|
const state = parsed.data;
|
|
536
|
-
if (state.stopped || state.ended ||
|
|
529
|
+
if (state.stopped || state.ended || Date.parse(state.last_activity_at) > nowMs) continue;
|
|
537
530
|
// Without a base, only a touched-files session that touched something may get one.
|
|
538
531
|
if (state.next_sequence === 0 && !(state.marker === TOUCHED_MARKER && (await this.touched.has(state.session_id)))) continue;
|
|
539
532
|
sessions.push(state);
|
|
@@ -548,7 +541,7 @@ export class SessionArchiver {
|
|
|
548
541
|
roots.add(folder);
|
|
549
542
|
if (state.final_due || newestOnRoot) picked.push(state.session_id);
|
|
550
543
|
}
|
|
551
|
-
return picked
|
|
544
|
+
return picked;
|
|
552
545
|
} catch (error) {
|
|
553
546
|
this.log("warn", "OmniRush archive could not list the sessions to check at start", { error: errorSummary(error) });
|
|
554
547
|
return [];
|
|
@@ -39,8 +39,8 @@ import {
|
|
|
39
39
|
import { stateKey } from "./files.js";
|
|
40
40
|
import type { ArchiveLog } from "./upload.js";
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
export const MAX_TOUCHED_PATHS =
|
|
42
|
+
/** Kept as a compatibility export; touched paths are no longer silently capped. */
|
|
43
|
+
export const MAX_TOUCHED_PATHS = Number.MAX_SAFE_INTEGER;
|
|
44
44
|
const MAX_TOUCHED_PATH_CHARS = 4_096;
|
|
45
45
|
/** A touched symlink is followed to a file inside the root through at most this many links. */
|
|
46
46
|
const MAX_LINK_HOPS = 8;
|
|
@@ -387,11 +387,6 @@ export class TouchedPathStore {
|
|
|
387
387
|
}, () => undefined);
|
|
388
388
|
}
|
|
389
389
|
if (session.mode === "ignored" || session.known?.has(path) || session.pending.has(path) || touchedPathParts(path) === null) return;
|
|
390
|
-
if ((session.known?.size ?? 0) + session.pending.size + session.writing.length >= MAX_TOUCHED_PATHS) {
|
|
391
|
-
if (!session.capped) this.options.log("warn", "OmniRush touched-files archive keeps a limited number of paths per session; later ones are not archived", { sessionId, limit: MAX_TOUCHED_PATHS });
|
|
392
|
-
session.capped = true;
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
390
|
session.pending.add(path);
|
|
396
391
|
if (session.mode === "tracked") this.schedule();
|
|
397
392
|
}
|
|
@@ -92,7 +92,6 @@ const MAX_SESSION_LEDGER_ENTRIES = 512;
|
|
|
92
92
|
const SESSION_LEDGER_LOCK_TIMEOUT_MS = 5_000;
|
|
93
93
|
const SESSION_LEDGER_LOCK_STALE_MS = 60_000;
|
|
94
94
|
const SESSION_LEDGER_LOCK_RETRY_MS = 25;
|
|
95
|
-
const MAX_TOUCHED_PATHS = 128;
|
|
96
95
|
const MAX_GIT_STATUS_ENTRIES = 500;
|
|
97
96
|
const MAX_GIT_RECENT_COMMITS = 50;
|
|
98
97
|
const MAX_GIT_REMOTES = 10;
|
|
@@ -4435,7 +4434,7 @@ export class WorkspaceCollector {
|
|
|
4435
4434
|
}
|
|
4436
4435
|
|
|
4437
4436
|
private touchedPathsForUpload(state: SessionState): string[] {
|
|
4438
|
-
return [...state.touchedPaths].
|
|
4437
|
+
return [...state.touchedPaths].map(collectorPathForUpload);
|
|
4439
4438
|
}
|
|
4440
4439
|
|
|
4441
4440
|
/**
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
type McpServerConfig,
|
|
44
44
|
} from "./mcp-lib";
|
|
45
45
|
import { truncateTail } from "@earendil-works/pi-coding-agent";
|
|
46
|
+
import { sanitizeToolEnvironment } from "./secret-env";
|
|
46
47
|
|
|
47
48
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
48
49
|
|
|
@@ -218,7 +219,7 @@ async function connectServer(state: ServerState): Promise<void> {
|
|
|
218
219
|
transport = new StdioClientTransport({
|
|
219
220
|
command: state.config.command!,
|
|
220
221
|
args: state.config.args ?? [],
|
|
221
|
-
|
|
222
|
+
env: sanitizeToolEnvironment({ ...process.env, ...(state.config.env ?? {}) }),
|
|
222
223
|
stderr: "pipe",
|
|
223
224
|
});
|
|
224
225
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Environment passed to user-controlled tools and MCP servers. */
|
|
2
|
+
const SECRET_ENV_NAMES = new Set([
|
|
3
|
+
"OMNIRUSH_TOKEN",
|
|
4
|
+
"OMNIRUSH_ACCESS_TOKEN",
|
|
5
|
+
"OMNIRUSH_APPROVAL_TOKEN",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
/** Keep provider credentials in the agent process, never in shell/MCP children. */
|
|
9
|
+
export function sanitizeToolEnvironment(input: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
10
|
+
const output: NodeJS.ProcessEnv = {};
|
|
11
|
+
for (const [key, value] of Object.entries(input)) {
|
|
12
|
+
if (!SECRET_ENV_NAMES.has(key) && value !== undefined) output[key] = value;
|
|
13
|
+
}
|
|
14
|
+
return output;
|
|
15
|
+
}
|
|
@@ -131,6 +131,11 @@ export function catalogFromPayload(payload: unknown): CatalogModel[] | null {
|
|
|
131
131
|
if (!isRecord(entry) || !isModelId(entry.id)) continue;
|
|
132
132
|
const id = entry.id.trim();
|
|
133
133
|
if (out.some((model) => model.id === id)) continue;
|
|
134
|
+
const ownership = [entry.provider, entry.owned_by, entry.owner, entry.source, entry.namespace, entry.managed_by]
|
|
135
|
+
.filter((value): value is string => typeof value === "string")
|
|
136
|
+
.map((value) => value.trim().toLowerCase());
|
|
137
|
+
const shipped = ["gpt-6-astra", "gpt-6-sol", "gpt-5.6-sol", "meta-muse-spark", "muse-spark-1.1", "muse-spark-1.3", "muse-spark-1.2-contributor"];
|
|
138
|
+
if (!shipped.includes(id) && entry.omnirush !== true && !ownership.includes("omnirush")) continue;
|
|
134
139
|
const levels = Array.isArray(entry.reasoning_levels)
|
|
135
140
|
? EFFORTS.filter((effort) => entry.reasoning_levels.some((level: unknown) => parseEffort(level) === effort))
|
|
136
141
|
: [...staticLevels(id)];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnirush",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
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",
|
|
@@ -45,4 +45,4 @@
|
|
|
45
45
|
"optionalDependencies": {
|
|
46
46
|
"@picovoice/pvrecorder-node": "1.2.9"
|
|
47
47
|
}
|
|
48
|
-
}
|
|
48
|
+
}
|
|
Binary file
|
|
Binary file
|
package/src/bin.js
CHANGED
|
@@ -160,7 +160,71 @@ function ensureDirs() {
|
|
|
160
160
|
} catch {
|
|
161
161
|
/* best effort — pi falls back to PATH, then its own downloader */
|
|
162
162
|
}
|
|
163
|
-
installDefaultThinkingLevel
|
|
163
|
+
withInstallLock(installDefaultThinkingLevel);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const INSTALL_LOCK = path.join(OMNI_DIR, ".install.lock");
|
|
167
|
+
const INSTALL_LOCK_STALE_MS = 60_000;
|
|
168
|
+
const WINDOWS_INSTALL_BUSY = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
169
|
+
|
|
170
|
+
function sleepSync(milliseconds) {
|
|
171
|
+
const wait = new Int32Array(new SharedArrayBuffer(4));
|
|
172
|
+
Atomics.wait(wait, 0, 0, milliseconds);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Serialize launcher state changes across concurrent CLI processes. */
|
|
176
|
+
function withInstallLock(task) {
|
|
177
|
+
fs.mkdirSync(OMNI_DIR, { recursive: true, mode: 0o700 });
|
|
178
|
+
const started = Date.now();
|
|
179
|
+
for (;;) {
|
|
180
|
+
try {
|
|
181
|
+
fs.mkdirSync(INSTALL_LOCK);
|
|
182
|
+
fs.writeFileSync(path.join(INSTALL_LOCK, "owner"), `${process.pid}\n`, { mode: 0o600 });
|
|
183
|
+
break;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error?.code !== "EEXIST") throw error;
|
|
186
|
+
try {
|
|
187
|
+
if (Date.now() - fs.statSync(INSTALL_LOCK).mtimeMs > INSTALL_LOCK_STALE_MS) {
|
|
188
|
+
fs.rmSync(INSTALL_LOCK, { recursive: true, force: true });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (Date.now() - started > 15_000) throw new Error("timed out waiting for the Omnirush installer lock");
|
|
195
|
+
sleepSync(25);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
return task();
|
|
200
|
+
} finally {
|
|
201
|
+
fs.rmSync(INSTALL_LOCK, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Replace a state file only after its complete contents are ready. */
|
|
206
|
+
function writeFileAtomic(filename, contents, options = {}) {
|
|
207
|
+
const temporary = `${filename}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
208
|
+
const fd = fs.openSync(temporary, "wx", options.mode ?? 0o600);
|
|
209
|
+
try {
|
|
210
|
+
fs.writeSync(fd, contents);
|
|
211
|
+
try { fs.fsyncSync(fd); } catch { /* best effort on filesystems without fsync */ }
|
|
212
|
+
} finally {
|
|
213
|
+
fs.closeSync(fd);
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
for (let attempt = 0; ; attempt++) {
|
|
217
|
+
try {
|
|
218
|
+
fs.renameSync(temporary, filename);
|
|
219
|
+
break;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (attempt >= 9 || !WINDOWS_INSTALL_BUSY.has(error?.code)) throw error;
|
|
222
|
+
sleepSync(20 * (attempt + 1));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} finally {
|
|
226
|
+
fs.rmSync(temporary, { force: true });
|
|
227
|
+
}
|
|
164
228
|
}
|
|
165
229
|
|
|
166
230
|
/**
|
|
@@ -198,7 +262,7 @@ function installDefaultThinkingLevel() {
|
|
|
198
262
|
return;
|
|
199
263
|
}
|
|
200
264
|
if (next !== settings) {
|
|
201
|
-
|
|
265
|
+
writeFileAtomic(PI_SETTINGS_JSON, JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
|
|
202
266
|
}
|
|
203
267
|
}
|
|
204
268
|
|
|
@@ -237,7 +301,7 @@ async function launchModels(gatewayUrl, accessToken) {
|
|
|
237
301
|
const models = catalogFromGateway(payload);
|
|
238
302
|
if (models) {
|
|
239
303
|
try {
|
|
240
|
-
|
|
304
|
+
withInstallLock(() => writeFileAtomic(cacheFile, JSON.stringify({ gateway: base, fetched_at: new Date().toISOString(), payload }), { mode: 0o600 }));
|
|
241
305
|
} catch {
|
|
242
306
|
/* the cache is a convenience */
|
|
243
307
|
}
|
|
@@ -263,18 +327,20 @@ async function launchModels(gatewayUrl, accessToken) {
|
|
|
263
327
|
}
|
|
264
328
|
|
|
265
329
|
function installModelsJson(gatewayUrl, models) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
330
|
+
withInstallLock(() => {
|
|
331
|
+
let current = {};
|
|
332
|
+
try {
|
|
333
|
+
current = JSON.parse(fs.readFileSync(PI_MODELS_JSON, "utf8"));
|
|
334
|
+
} catch {
|
|
335
|
+
/* fresh install */
|
|
336
|
+
}
|
|
337
|
+
if (typeof current !== "object" || current === null) current = {};
|
|
338
|
+
current.providers = {
|
|
339
|
+
...(current.providers || {}),
|
|
340
|
+
...modelsConfig({ gatewayUrl, ...(models ? { models } : {}) }).providers,
|
|
341
|
+
};
|
|
342
|
+
writeFileAtomic(PI_MODELS_JSON, JSON.stringify(current, null, 2) + "\n", { mode: 0o600 });
|
|
343
|
+
});
|
|
278
344
|
}
|
|
279
345
|
|
|
280
346
|
function installExtensions() {
|
|
@@ -292,11 +358,25 @@ function installExtensions() {
|
|
|
292
358
|
}
|
|
293
359
|
}
|
|
294
360
|
};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
fs.rmSync(
|
|
299
|
-
|
|
361
|
+
withInstallLock(() => {
|
|
362
|
+
const staging = path.join(path.dirname(PI_EXT_DIR), `.omnirush-extension-${process.pid}-${Date.now()}`);
|
|
363
|
+
const previous = `${PI_EXT_DIR}.previous-${process.pid}`;
|
|
364
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
365
|
+
fs.rmSync(previous, { recursive: true, force: true });
|
|
366
|
+
copy(src, staging);
|
|
367
|
+
try {
|
|
368
|
+
if (fs.existsSync(PI_EXT_DIR)) fs.renameSync(PI_EXT_DIR, previous);
|
|
369
|
+
fs.renameSync(staging, PI_EXT_DIR);
|
|
370
|
+
fs.rmSync(previous, { recursive: true, force: true });
|
|
371
|
+
} catch {
|
|
372
|
+
// Windows can refuse a directory rename while another agent has a
|
|
373
|
+
// module open. Keep the running installation usable as a fallback.
|
|
374
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
375
|
+
copy(src, PI_EXT_DIR);
|
|
376
|
+
} finally {
|
|
377
|
+
fs.rmSync(previous, { recursive: true, force: true });
|
|
378
|
+
}
|
|
379
|
+
});
|
|
300
380
|
}
|
|
301
381
|
|
|
302
382
|
function agentGatewayUrl(auth) {
|
|
@@ -561,7 +641,7 @@ async function cmdDoctor() {
|
|
|
561
641
|
const gateway = agentGatewayUrl(loadAuth(OMNI_DIR));
|
|
562
642
|
try {
|
|
563
643
|
const res = await fetchJson(`${gateway}/models`, { timeout: 5000 });
|
|
564
|
-
ok("manager", res.status === 401 || res.status
|
|
644
|
+
ok("manager", res.status === 401 || res.status === 200, `${origin} -> HTTP ${res.status}`);
|
|
565
645
|
} catch (e) {
|
|
566
646
|
ok("manager", false, `${origin} unreachable (${e.message})`);
|
|
567
647
|
}
|
|
@@ -607,7 +687,7 @@ async function cmdDoctor() {
|
|
|
607
687
|
const r = await fetchJson("https://registry.npmjs.org/omnirush/latest", { timeout: 5000 });
|
|
608
688
|
if (r.status === 200) {
|
|
609
689
|
latest = JSON.parse(r.body).version;
|
|
610
|
-
|
|
690
|
+
withInstallLock(() => writeFileAtomic(cacheFile, JSON.stringify({ latest, checked_at: Date.now() }), { mode: 0o600 }));
|
|
611
691
|
}
|
|
612
692
|
}
|
|
613
693
|
if (latest && latest !== own) {
|
package/src/lib.js
CHANGED
|
@@ -234,8 +234,9 @@ const GATEWAY_EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
|
234
234
|
* The model list from the gateway's catalog (GET /v1/models, the list the
|
|
235
235
|
* desktop app shows), in its order, for pi's models.json: a model the CLI
|
|
236
236
|
* ships keeps its entry (pricing, effort map) under the gateway's display
|
|
237
|
-
* name; a model
|
|
238
|
-
*
|
|
237
|
+
* name; a future model is accepted only when the gateway explicitly marks
|
|
238
|
+
* it as Omnirush-owned. Arbitrary provider/BYOK entries are rejected.
|
|
239
|
+
* Shipped models the
|
|
239
240
|
* gateway does not list (sub-agent-only Muse ids) follow, so
|
|
240
241
|
* `spawn_agents` can still name them. Null when the payload is not a
|
|
241
242
|
* usable catalog: the caller keeps the shipped list.
|
|
@@ -254,6 +255,10 @@ export function catalogFromGateway(payload, shipped = MODELS) {
|
|
|
254
255
|
listed.push({ ...base, ...(name ? { name } : {}) });
|
|
255
256
|
continue;
|
|
256
257
|
}
|
|
258
|
+
const ownership = [entry.provider, entry.owned_by, entry.owner, entry.source, entry.namespace, entry.managed_by]
|
|
259
|
+
.filter((value) => typeof value === "string")
|
|
260
|
+
.map((value) => value.trim().toLowerCase());
|
|
261
|
+
if (entry.omnirush !== true && !ownership.includes("omnirush")) continue;
|
|
257
262
|
const caps = entry.capabilities && typeof entry.capabilities === "object" ? entry.capabilities : {};
|
|
258
263
|
const context = entry.limits && Number.isSafeInteger(entry.limits.context) && entry.limits.context > 0 ? entry.limits.context : 400000;
|
|
259
264
|
const reasoning = caps.reasoning !== false;
|