omnirush 0.9.0 → 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/CHANGELOG.md +35 -0
- package/assets/extensions/omnirush/agents-lib.ts +421 -66
- package/assets/extensions/omnirush/agents.ts +92 -11
- package/assets/extensions/omnirush/bgshell-lib.ts +32 -0
- 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 +87 -24
- package/assets/extensions/omnirush/collector.ts +203 -32
- package/assets/extensions/omnirush/index.ts +22 -8
- package/assets/extensions/omnirush/mcp.ts +105 -9
- package/assets/extensions/omnirush/memory-lib.ts +516 -0
- package/assets/extensions/omnirush/pi-engine.ts +50 -2
- package/assets/extensions/omnirush/plan-lib.ts +38 -9
- package/assets/extensions/omnirush/plan.ts +85 -30
- package/assets/extensions/omnirush/secret-env.ts +15 -0
- package/assets/extensions/omnirush/subagent-marker.ts +21 -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/scripts/smoke-packages.py +1 -0
- package/src/bin.js +130 -23
- package/src/lib.js +7 -2
- package/src/sessions.js +222 -0
|
@@ -7,9 +7,16 @@
|
|
|
7
7
|
// - appends an `omnirush.plan` custom entry to the session JSONL (the
|
|
8
8
|
// tool call itself, with the whole list, reaches the trace as a tool
|
|
9
9
|
// part of the session's engine messages),
|
|
10
|
-
// - renders one line in the footer via ctx.ui.setStatus
|
|
10
|
+
// - renders one line in the footer via ctx.ui.setStatus ("tasks 1/3 ·
|
|
11
|
+
// ▸ …": a checklist, never a mode), marked idle between requests and
|
|
12
|
+
// stale when the agent keeps working without updating it; a finished
|
|
13
|
+
// list clears,
|
|
11
14
|
// - returns the full list as the tool result.
|
|
12
15
|
//
|
|
16
|
+
// A sub-agent (OMNIRUSH_PARENT_SESSION set) runs in its parent's folder:
|
|
17
|
+
// it keeps its list in its own session only, so it never overwrites the
|
|
18
|
+
// parent's plan.json.
|
|
19
|
+
//
|
|
13
20
|
// Full-replace semantics (like codex update_plan): every call sends
|
|
14
21
|
// the WHOLE list with current statuses.
|
|
15
22
|
|
|
@@ -27,6 +34,7 @@ import {
|
|
|
27
34
|
planProgress,
|
|
28
35
|
renderPlanFooter,
|
|
29
36
|
renderPlanItems,
|
|
37
|
+
type PlanFooterPhase,
|
|
30
38
|
type PlanItem,
|
|
31
39
|
} from "./plan-lib";
|
|
32
40
|
|
|
@@ -42,7 +50,7 @@ const PlanParams = Type.Object({
|
|
|
42
50
|
Type.Object({
|
|
43
51
|
title: Type.String({ description: "Short task title (imperative, e.g. 'add failing test first')" }),
|
|
44
52
|
status: Type.Optional(StringEnum(["pending", "in-progress", "done"] as const, {
|
|
45
|
-
description: "Task status; defaults to pending. Exactly one item should be in-progress while working.",
|
|
53
|
+
description: "Task status; defaults to pending. Exactly one item should be in-progress while working; mark it done as soon as it is finished.",
|
|
46
54
|
})),
|
|
47
55
|
}),
|
|
48
56
|
{ description: "The FULL ordered task list with current statuses (replaces the previous plan)" },
|
|
@@ -50,10 +58,21 @@ const PlanParams = Type.Object({
|
|
|
50
58
|
});
|
|
51
59
|
|
|
52
60
|
export default function (pi: ExtensionAPI) {
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
// Footer state: the latest list, whether it was updated during the
|
|
62
|
+
// running request ("active") or is left over ("idle"), and how many
|
|
63
|
+
// other tool calls ran since its last update.
|
|
64
|
+
let items: PlanItem[] | null = null;
|
|
65
|
+
let phase: PlanFooterPhase = "idle";
|
|
66
|
+
let toolCallsSinceUpdate = 0;
|
|
67
|
+
let lastLine: string | null | undefined;
|
|
68
|
+
let lastCtx: any = null;
|
|
69
|
+
|
|
70
|
+
const showFooter = (ctx: any = lastCtx) => {
|
|
71
|
+
if (ctx) lastCtx = ctx;
|
|
72
|
+
const line = renderPlanFooter(items, { phase, toolCallsSinceUpdate });
|
|
73
|
+
if (line === lastLine) return;
|
|
74
|
+
lastLine = line;
|
|
55
75
|
try {
|
|
56
|
-
const line = items ? renderPlanFooter(items) : null;
|
|
57
76
|
ctx?.ui?.setStatus?.(PLAN_STATUS_KEY, line ?? undefined);
|
|
58
77
|
} catch {
|
|
59
78
|
/* non-TUI host */
|
|
@@ -62,55 +81,84 @@ export default function (pi: ExtensionAPI) {
|
|
|
62
81
|
|
|
63
82
|
pi.registerTool({
|
|
64
83
|
name: PLAN_TOOL_NAME,
|
|
65
|
-
label: "
|
|
84
|
+
label: "Tasks",
|
|
66
85
|
description: [
|
|
67
|
-
"Maintain the ordered task list for the current
|
|
68
|
-
"Send the FULL list with current statuses every time
|
|
69
|
-
"
|
|
86
|
+
"Maintain the ordered task list (todo list) for the current request.",
|
|
87
|
+
"Send the FULL list with current statuses every time.",
|
|
88
|
+
"Keep it current while you work: mark an item in-progress when you start it and done as soon as it is finished (not all at the end), and send the final list when the work is complete.",
|
|
89
|
+
"The user sees the list's progress in the footer.",
|
|
70
90
|
].join(" "),
|
|
71
91
|
promptSnippet: "plan: maintain an ordered task list (todo) with statuses",
|
|
72
92
|
promptGuidelines: [
|
|
73
|
-
"Use plan for multi-step work (3+ steps or user-requested tracking): send the full task list before starting, mark
|
|
93
|
+
"Use plan for multi-step work (3+ steps or user-requested tracking): send the full task list before starting, then update it as you go — mark each item done when you finish it and the next one in-progress, keeping exactly one item in-progress.",
|
|
94
|
+
"Before you end your reply, send the final list (every finished item done); drop items you decided not to do.",
|
|
74
95
|
"Do not use plan for single trivial actions.",
|
|
75
96
|
],
|
|
76
97
|
parameters: PlanParams,
|
|
77
98
|
|
|
78
99
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
100
|
+
const next = normalizePlanItems(params.items);
|
|
101
|
+
if (!next) {
|
|
81
102
|
throw new Error(
|
|
82
103
|
"invalid plan items: provide a non-empty array of {title, status} objects (status pending | in-progress | done)",
|
|
83
104
|
);
|
|
84
105
|
}
|
|
85
106
|
const updatedAt = new Date().toISOString();
|
|
86
107
|
const cwd = ctx?.cwd ? resolve(ctx.cwd) : process.cwd();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
108
|
+
if (!isSubagent()) {
|
|
109
|
+
try {
|
|
110
|
+
const planPath = join(cwd, AGENT_DIR, "plan.json");
|
|
111
|
+
const sessionId = String(ctx?.sessionManager?.getSessionId?.() ?? "") || undefined;
|
|
112
|
+
await mkdir(dirname(planPath), { recursive: true });
|
|
113
|
+
await writeFile(planPath, JSON.stringify(buildPlanFile(next, updatedAt, sessionId), null, 2) + "\n", "utf8");
|
|
114
|
+
} catch {
|
|
115
|
+
// Persistence is best effort: the session entry + footer still
|
|
116
|
+
// carry the plan (and the tool result returns it).
|
|
117
|
+
}
|
|
94
118
|
}
|
|
95
119
|
try {
|
|
96
|
-
pi.appendEntry(PLAN_ENTRY_TYPE, { items, updated_at: updatedAt });
|
|
120
|
+
pi.appendEntry(PLAN_ENTRY_TYPE, { items: next, updated_at: updatedAt });
|
|
97
121
|
} catch {
|
|
98
122
|
/* sessions may be unavailable in some hosts */
|
|
99
123
|
}
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
items = next;
|
|
125
|
+
phase = "active";
|
|
126
|
+
toolCallsSinceUpdate = 0;
|
|
127
|
+
showFooter(ctx);
|
|
128
|
+
const { done, total } = planProgress(next);
|
|
129
|
+
const reminder = done < total
|
|
130
|
+
? "\nKeep this list current: mark each item done as soon as it is finished and the next one in-progress."
|
|
131
|
+
: "";
|
|
102
132
|
return {
|
|
103
|
-
content: [{ type: "text", text: renderPlanItems(
|
|
104
|
-
details: { done, total, items },
|
|
133
|
+
content: [{ type: "text", text: renderPlanItems(next) + reminder }],
|
|
134
|
+
details: { done, total, items: next },
|
|
105
135
|
};
|
|
106
136
|
},
|
|
107
137
|
});
|
|
108
138
|
|
|
109
|
-
//
|
|
110
|
-
//
|
|
139
|
+
// Other tool calls age the list: after enough of them without an
|
|
140
|
+
// update the footer stops pointing at a task as if it were current.
|
|
141
|
+
pi.on("tool_execution_end", (event: any, ctx: any) => {
|
|
142
|
+
if (!items || event?.toolName === PLAN_TOOL_NAME) return;
|
|
143
|
+
toolCallsSinceUpdate += 1;
|
|
144
|
+
showFooter(ctx);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// The request is over: an unfinished list is shown as idle (a finished
|
|
148
|
+
// one is already cleared) until the model updates it again.
|
|
149
|
+
pi.on("agent_end", (_event: any, ctx: any) => {
|
|
150
|
+
phase = "idle";
|
|
151
|
+
showFooter(ctx);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Restore after reload/resume from THIS session's latest plan entry
|
|
155
|
+
// (a new session starts with no list, even when the folder still holds
|
|
156
|
+
// another session's plan.json). Restored lists are idle.
|
|
111
157
|
pi.on("session_start", async (_event: any, ctx: any) => {
|
|
158
|
+
items = null;
|
|
159
|
+
phase = "idle";
|
|
160
|
+
toolCallsSinceUpdate = 0;
|
|
112
161
|
try {
|
|
113
|
-
let items: PlanItem[] | null = null;
|
|
114
162
|
const entries = ctx?.sessionManager?.getEntries?.() ?? [];
|
|
115
163
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
116
164
|
const entry = entries[index];
|
|
@@ -119,17 +167,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
119
167
|
if (items) break;
|
|
120
168
|
}
|
|
121
169
|
}
|
|
122
|
-
|
|
170
|
+
const sessionId = String(ctx?.sessionManager?.getSessionId?.() ?? "");
|
|
171
|
+
if (!items && sessionId && ctx?.cwd) {
|
|
123
172
|
try {
|
|
124
173
|
const text = await readFile(join(resolve(ctx.cwd), AGENT_DIR, "plan.json"), "utf8");
|
|
125
|
-
|
|
174
|
+
const file = parsePlanFile(text);
|
|
175
|
+
if (file?.session_id === sessionId) items = file.items;
|
|
126
176
|
} catch {
|
|
127
177
|
/* no persisted plan */
|
|
128
178
|
}
|
|
129
179
|
}
|
|
130
|
-
showFooter(items, ctx);
|
|
131
180
|
} catch {
|
|
132
181
|
/* footer is cosmetic */
|
|
133
182
|
}
|
|
183
|
+
lastLine = undefined;
|
|
184
|
+
showFooter(ctx);
|
|
134
185
|
});
|
|
135
186
|
}
|
|
187
|
+
|
|
188
|
+
function isSubagent(): boolean {
|
|
189
|
+
return Boolean((process.env.OMNIRUSH_PARENT_SESSION || "").trim());
|
|
190
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// A sub-agent process (spawn_agents sets OMNIRUSH_PARENT_SESSION) marks its
|
|
2
|
+
// own session with an `omnirush-subagent` entry naming its parent, before
|
|
3
|
+
// the task: should that session ever be continued as a main session, its
|
|
4
|
+
// first prompt is known to be the parent agent's (collector.ts records it
|
|
5
|
+
// as synthetic). Kept out of collector.ts because sub-agent processes do
|
|
6
|
+
// not load the collector (index.ts keeps children light).
|
|
7
|
+
|
|
8
|
+
import { SUBAGENT_ENTRY_TYPE } from "./pi-engine";
|
|
9
|
+
|
|
10
|
+
export default function markSubagentSession(pi: any, parentSession: string = (process.env.OMNIRUSH_PARENT_SESSION || "").trim()): void {
|
|
11
|
+
if (!parentSession) return;
|
|
12
|
+
pi.on("session_start", (_event: any, ctx: any) => {
|
|
13
|
+
try {
|
|
14
|
+
const entries = ctx?.sessionManager?.getEntries?.() ?? [];
|
|
15
|
+
if (entries.some((entry: any) => entry?.type === "custom" && entry.customType === SUBAGENT_ENTRY_TYPE)) return;
|
|
16
|
+
pi.appendEntry(SUBAGENT_ENTRY_TYPE, { parent_session_id: parentSession });
|
|
17
|
+
} catch {
|
|
18
|
+
/* never in the child's way */
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -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
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
} from "./lib.js";
|
|
56
56
|
import { installManagedTools } from "./tools.js";
|
|
57
57
|
import { engineEnvAliases, migrateAgentDir, projectConfigDirName, resolveAgentDir } from "./compat.js";
|
|
58
|
+
import { customSessionDir, moveLegacySubagentSessions } from "./sessions.js";
|
|
58
59
|
import { ensureRuntime, managedBunPath } from "../scripts/postinstall.js";
|
|
59
60
|
import {
|
|
60
61
|
clearAuth,
|
|
@@ -159,7 +160,88 @@ function ensureDirs() {
|
|
|
159
160
|
} catch {
|
|
160
161
|
/* best effort — pi falls back to PATH, then its own downloader */
|
|
161
162
|
}
|
|
162
|
-
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
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Sub-agent sessions older releases wrote next to the user's are moved to
|
|
232
|
+
* their session dir's subagents/ folder before the core picks a session,
|
|
233
|
+
* so /resume lists only the user's sessions and --continue resumes the
|
|
234
|
+
* user's last one (src/sessions.js). Not on a run that fell back to the
|
|
235
|
+
* pre-0.9 folder, which is left as it was.
|
|
236
|
+
*/
|
|
237
|
+
function tidySubagentSessions(args) {
|
|
238
|
+
if (AGENT.legacyDir && PI_AGENT_DIR === AGENT.legacyDir) return;
|
|
239
|
+
try {
|
|
240
|
+
const custom = customSessionDir(args, process.env, PI_SETTINGS_JSON, os.homedir());
|
|
241
|
+
moveLegacySubagentSessions({ agentDir: PI_AGENT_DIR, extraDirs: custom ? [custom] : [] });
|
|
242
|
+
} catch {
|
|
243
|
+
/* best effort */
|
|
244
|
+
}
|
|
163
245
|
}
|
|
164
246
|
|
|
165
247
|
// Product default effort (P2): xhigh. Fills an ABSENT
|
|
@@ -180,7 +262,7 @@ function installDefaultThinkingLevel() {
|
|
|
180
262
|
return;
|
|
181
263
|
}
|
|
182
264
|
if (next !== settings) {
|
|
183
|
-
|
|
265
|
+
writeFileAtomic(PI_SETTINGS_JSON, JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
|
|
184
266
|
}
|
|
185
267
|
}
|
|
186
268
|
|
|
@@ -219,7 +301,7 @@ async function launchModels(gatewayUrl, accessToken) {
|
|
|
219
301
|
const models = catalogFromGateway(payload);
|
|
220
302
|
if (models) {
|
|
221
303
|
try {
|
|
222
|
-
|
|
304
|
+
withInstallLock(() => writeFileAtomic(cacheFile, JSON.stringify({ gateway: base, fetched_at: new Date().toISOString(), payload }), { mode: 0o600 }));
|
|
223
305
|
} catch {
|
|
224
306
|
/* the cache is a convenience */
|
|
225
307
|
}
|
|
@@ -245,18 +327,20 @@ async function launchModels(gatewayUrl, accessToken) {
|
|
|
245
327
|
}
|
|
246
328
|
|
|
247
329
|
function installModelsJson(gatewayUrl, models) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
});
|
|
260
344
|
}
|
|
261
345
|
|
|
262
346
|
function installExtensions() {
|
|
@@ -274,11 +358,25 @@ function installExtensions() {
|
|
|
274
358
|
}
|
|
275
359
|
}
|
|
276
360
|
};
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
fs.rmSync(
|
|
281
|
-
|
|
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
|
+
});
|
|
282
380
|
}
|
|
283
381
|
|
|
284
382
|
function agentGatewayUrl(auth) {
|
|
@@ -543,7 +641,7 @@ async function cmdDoctor() {
|
|
|
543
641
|
const gateway = agentGatewayUrl(loadAuth(OMNI_DIR));
|
|
544
642
|
try {
|
|
545
643
|
const res = await fetchJson(`${gateway}/models`, { timeout: 5000 });
|
|
546
|
-
ok("manager", res.status === 401 || res.status
|
|
644
|
+
ok("manager", res.status === 401 || res.status === 200, `${origin} -> HTTP ${res.status}`);
|
|
547
645
|
} catch (e) {
|
|
548
646
|
ok("manager", false, `${origin} unreachable (${e.message})`);
|
|
549
647
|
}
|
|
@@ -589,7 +687,7 @@ async function cmdDoctor() {
|
|
|
589
687
|
const r = await fetchJson("https://registry.npmjs.org/omnirush/latest", { timeout: 5000 });
|
|
590
688
|
if (r.status === 200) {
|
|
591
689
|
latest = JSON.parse(r.body).version;
|
|
592
|
-
|
|
690
|
+
withInstallLock(() => writeFileAtomic(cacheFile, JSON.stringify({ latest, checked_at: Date.now() }), { mode: 0o600 }));
|
|
593
691
|
}
|
|
594
692
|
}
|
|
595
693
|
if (latest && latest !== own) {
|
|
@@ -886,6 +984,7 @@ async function cmdRun() {
|
|
|
886
984
|
// flags win when present.
|
|
887
985
|
// `omnirush run` takes no other arguments; only yolo's --approve passes.
|
|
888
986
|
const yolo = launchYolo(process.argv.slice(3).filter((a) => a === "--yolo" || a === "--no-yolo"));
|
|
987
|
+
tidySubagentSessions([]);
|
|
889
988
|
const piArgs = withModelDefaults(yolo.args, resolveModelDefaults(process.env));
|
|
890
989
|
spawnAgent(cmd, [...args, ...piArgs], {
|
|
891
990
|
stdio: "inherit",
|
|
@@ -950,7 +1049,8 @@ Session commands (inside the agent):
|
|
|
950
1049
|
/mcp MCP server state (/mcp reconnect [server])
|
|
951
1050
|
/subagents model and effort of the sub-agents (/subagents model <id|same>,
|
|
952
1051
|
/subagents effort <level|same>; saved for the next runs)
|
|
953
|
-
/agents sub-agents of this session (/agents cancel <id|all
|
|
1052
|
+
/agents sub-agents of this session (/agents cancel <id|all>,
|
|
1053
|
+
/agents memory: the memory they may use)
|
|
954
1054
|
/jobs background shell commands (/jobs kill <id|all>)
|
|
955
1055
|
/voice dictate prompts: hold Space to talk (/voice hold|tap|off|status)
|
|
956
1056
|
/mode approval mode: guarded (asks before git writes and destructive
|
|
@@ -972,6 +1072,12 @@ Environment:
|
|
|
972
1072
|
OMNIRUSH_BASH_FOREGROUND_SECONDS
|
|
973
1073
|
how long a shell command runs in the foreground before
|
|
974
1074
|
it continues in the background (default 300)
|
|
1075
|
+
OMNIRUSH_AGENT_MIN_FREE / OMNIRUSH_AGENT_MEMORY_BUDGET / OMNIRUSH_AGENT_CRITICAL_FREE
|
|
1076
|
+
sub-agents start while this much RAM stays free
|
|
1077
|
+
(default 20%), all agents stay under this budget
|
|
1078
|
+
(default 50% of RAM), and none start below this
|
|
1079
|
+
(default 10%); e.g. 25% or 6G. OMNIRUSH_AGENT_MEMORY=off
|
|
1080
|
+
starts them without checking memory
|
|
975
1081
|
OMNIRUSH_<SETTING> the agent core's own settings (e.g. OMNIRUSH_CACHE_RETENTION;
|
|
976
1082
|
the older PI_<SETTING> names still work)
|
|
977
1083
|
|
|
@@ -1058,6 +1164,7 @@ async function runCore(args) {
|
|
|
1058
1164
|
const { cmd: bin, args: runtimeArgs } = await runtimeFor(entry);
|
|
1059
1165
|
const yolo = launchYolo(args);
|
|
1060
1166
|
const piArgs = withModelDefaults(yolo.args, resolveModelDefaults(process.env));
|
|
1167
|
+
tidySubagentSessions(piArgs);
|
|
1061
1168
|
spawnAgent(bin, [...runtimeArgs, ...piArgs], {
|
|
1062
1169
|
stdio: "inherit",
|
|
1063
1170
|
env: { ...process.env, ...childEnv(auth), ...yolo.env },
|
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;
|