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.
@@ -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
- /** Show the footer for a plan (or clear it when the plan is gone). */
54
- const showFooter = (items: PlanItem[] | null, ctx: any) => {
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: "Plan",
84
+ label: "Tasks",
66
85
  description: [
67
- "Maintain the ordered task list for the current work (todo list).",
68
- "Send the FULL list with current statuses every time; keep exactly one item in-progress while working and mark items done as you complete them.",
69
- "The list persists for the session and is visible to the user in the footer.",
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 items done as you finish them, and keep exactly one item in-progress.",
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 items = normalizePlanItems(params.items);
80
- if (!items) {
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
- try {
88
- const planPath = join(cwd, AGENT_DIR, "plan.json");
89
- await mkdir(dirname(planPath), { recursive: true });
90
- await writeFile(planPath, JSON.stringify(buildPlanFile(items, updatedAt), null, 2) + "\n", "utf8");
91
- } catch {
92
- // Persistence is best effort: the session entry + footer still
93
- // carry the plan (and the tool result returns it).
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
- showFooter(items, ctx);
101
- const { done, total } = planProgress(items);
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(items) }],
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
- // Restore the footer after reload/resume: the session's latest plan
110
- // entry wins, falling back to the persisted plan.json.
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
- if (!items && ctx?.cwd) {
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
- items = parsePlanFile(text)?.items ?? null;
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.9.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
+ }
@@ -28,6 +28,7 @@ REQUIRED_PATHS = [
28
28
  "src/bin.js",
29
29
  "src/lib.js",
30
30
  "src/compat.js",
31
+ "src/sessions.js",
31
32
  "scripts/postinstall.js",
32
33
  "scripts/brand-engine.js",
33
34
  "assets/extensions/omnirush/index.ts",
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
- fs.writeFileSync(PI_SETTINGS_JSON, JSON.stringify(next, null, 2) + "\n");
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
- fs.writeFileSync(cacheFile, JSON.stringify({ gateway: base, fetched_at: new Date().toISOString(), payload }), { mode: 0o600 });
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
- let current = {};
249
- try {
250
- current = JSON.parse(fs.readFileSync(PI_MODELS_JSON, "utf8"));
251
- } catch {
252
- /* fresh install */
253
- }
254
- if (typeof current !== "object" || current === null) current = {};
255
- current.providers = {
256
- ...(current.providers || {}),
257
- ...modelsConfig({ gatewayUrl, ...(models ? { models } : {}) }).providers,
258
- };
259
- fs.writeFileSync(PI_MODELS_JSON, JSON.stringify(current, null, 2) + "\n");
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
- copy(src, PI_EXT_DIR);
278
- // Modules of the earlier collector: nothing imports them any more.
279
- for (const stale of ["collector-lib.ts", "trace-format.ts"]) {
280
- fs.rmSync(path.join(PI_EXT_DIR, stale), { force: true });
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 < 500, `${origin} -> HTTP ${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
- fs.writeFileSync(cacheFile, JSON.stringify({ latest, checked_at: Date.now() }));
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 it does not know gets a generic entry from the catalog's
238
- * fields (context limit, image input, reasoning). Shipped models the
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;