omnirush 0.10.1 → 0.10.3
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 +66 -11
- package/assets/extensions/omnirush/agents.ts +85 -18
- package/assets/extensions/omnirush/auth.js +3 -2
- package/assets/extensions/omnirush/bgshell.ts +6 -6
- package/assets/extensions/omnirush/capture/UPSTREAM +7 -4
- package/assets/extensions/omnirush/capture/omnirush-swarm.ts +2 -2
- package/assets/extensions/omnirush/capture/server-fetch.ts +1 -1
- package/assets/extensions/omnirush/capture/session-archive/detect.ts +3 -3
- package/assets/extensions/omnirush/capture/session-archive/files.ts +1 -1
- package/assets/extensions/omnirush/capture/session-archive/ignored.ts +2 -2
- package/assets/extensions/omnirush/capture/session-archive/index.ts +8 -8
- package/assets/extensions/omnirush/capture/session-archive/lifecycle.ts +1 -1
- package/assets/extensions/omnirush/capture/session-archive/manifest.ts +4 -4
- package/assets/extensions/omnirush/capture/session-archive/touched.ts +2 -2
- package/assets/extensions/omnirush/capture/session-archive/upload.ts +3 -3
- package/assets/extensions/omnirush/capture/{collect-upload-budget.ts → sync-upload-budget.ts} +5 -5
- package/assets/extensions/omnirush/capture/turn-diff.ts +6 -6
- package/assets/extensions/omnirush/capture/{workspace-collector.ts → workspace-sync.ts} +253 -204
- package/assets/extensions/omnirush/commands.ts +6 -6
- package/assets/extensions/omnirush/{pi-engine.ts → engine-messages.ts} +7 -7
- package/assets/extensions/omnirush/index.ts +4 -4
- package/assets/extensions/omnirush/plan.ts +2 -2
- package/assets/extensions/omnirush/refresh.ts +1 -1
- package/assets/extensions/omnirush/retry.js +1 -1
- package/assets/extensions/omnirush/{collector.ts → session-sync.ts} +134 -58
- package/assets/extensions/omnirush/status-lib.ts +9 -9
- package/assets/extensions/omnirush/stream-timing.ts +4 -4
- package/assets/extensions/omnirush/subagent-marker.ts +4 -4
- package/assets/extensions/omnirush/subagents-lib.ts +113 -13
- package/assets/extensions/omnirush/subagents.ts +13 -2
- package/assets/extensions/omnirush/swarm-lib.ts +2 -2
- package/assets/extensions/omnirush/voice.ts +1 -1
- package/assets/{collect-once.ts → sync-once.ts} +20 -19
- package/package.json +1 -1
- package/src/bin.js +20 -8
- package/src/sessions.js +3 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// of the folder /resume and --continue read, so a user only ever sees
|
|
11
11
|
// their own conversations there), under a session id the parent
|
|
12
12
|
// picks (`--session-id`) and returns in the result (`session_id`): the
|
|
13
|
-
// parent session's
|
|
13
|
+
// parent session's session uploader reads the child's session file from it and
|
|
14
14
|
// records the child as a sub-agent of the turn (session.child events,
|
|
15
15
|
// grandchildren included), the way the desktop records task sub-agents.
|
|
16
16
|
//
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
ENV_FALLBACK_MODEL,
|
|
36
36
|
FALLBACK_MARKER,
|
|
37
37
|
fallbackNote,
|
|
38
|
+
sourceNote,
|
|
38
39
|
piThinking,
|
|
39
40
|
type SubagentFallback,
|
|
40
41
|
} from "./subagents-lib";
|
|
@@ -87,7 +88,7 @@ export const RELEASED_OUTPUT = "(output no longer kept in memory: it was deliver
|
|
|
87
88
|
/**
|
|
88
89
|
* Sub-agent layers below the main session (the desktop app's
|
|
89
90
|
* OMNIRUSH_SUBAGENT_DEPTH): layers 1 and 2 may delegate, layer 3 may not.
|
|
90
|
-
* The session capture records exactly these layers (
|
|
91
|
+
* The session capture records exactly these layers (session-sync.ts).
|
|
91
92
|
*/
|
|
92
93
|
export const MAX_SUBAGENT_DEPTH = 3;
|
|
93
94
|
/** The layer a process runs at, handed down to every child (+1 per layer). */
|
|
@@ -109,11 +110,23 @@ export function canDelegate(depth: number): boolean {
|
|
|
109
110
|
return depth < MAX_SUBAGENT_DEPTH;
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* The main session's spawn_agents guideline: delegate on request or on a
|
|
115
|
+
* clear split, exactly as many as the user names, each task only its own
|
|
116
|
+
* part (a pasted user message made the sub-agent obey the delegation
|
|
117
|
+
* instructions again and nest), all in one call.
|
|
118
|
+
*/
|
|
119
|
+
export const SPAWN_GUIDELINE = "Do simple work directly: a question that a few lookups or commands answer needs no sub-agent. Start sub-agents only when the user asks for them or the work clearly splits into independent parts that each take several tool calls; otherwise do the work yourself. When the user names a number of sub-agents, start exactly that many. Each task must be self-contained (the sub-agent cannot see this conversation) and limited to that sub-agent's part: say what to do, with the names, paths or URLs it needs, and what to report. Never paste the user's whole message or any instructions about sub-agents into a task. A sub-agent does its task itself; tell it to start sub-agents of its own only when the user explicitly asked for nested sub-agents. Start them all in one spawn_agents call so they run in parallel.";
|
|
120
|
+
|
|
121
|
+
/** A sub-agent's spawn_agents guideline (layer >= 1). */
|
|
122
|
+
export const SUBAGENT_SPAWN_GUIDELINE = "You are a sub-agent: do your whole task yourself, even when it is long. Use spawn_agents only if your task explicitly tells you to start sub-agents of your own.";
|
|
123
|
+
|
|
124
|
+
/** System text for a sub-agent: its layer, and that it does its task itself. */
|
|
113
125
|
export function subagentLayerNote(depth: number): string {
|
|
126
|
+
const head = `You are a sub-agent: layer ${depth} of at most ${MAX_SUBAGENT_DEPTH} below the main session. Do your whole task yourself, even when it is long: splitting it among sub-agents of your own is slower, not faster. Instructions about sub-agents in your task text (such as "use one subagent to ...") were meant for the main session, which has already carried them out: you are that sub-agent.`;
|
|
114
127
|
return canDelegate(depth)
|
|
115
|
-
?
|
|
116
|
-
:
|
|
128
|
+
? `${head} Start sub-agents (spawn_agents) only if your task explicitly tells you to start sub-agents of your own.`
|
|
129
|
+
: `${head} You cannot delegate further (there is no spawn_agents at this layer).`;
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
export const AGENT_ROLES = ["code-searcher", "researcher-web", "general-worker"] as const;
|
|
@@ -293,8 +306,8 @@ export interface ChildTask {
|
|
|
293
306
|
role: AgentRole;
|
|
294
307
|
task: string;
|
|
295
308
|
/** Cross-model children: gateway model id the child runs on
|
|
296
|
-
* (e.g. "muse-spark-1.
|
|
297
|
-
*
|
|
309
|
+
* (e.g. "muse-spark-1.1" workers under an astra parent). Undefined =
|
|
310
|
+
* inherit the parent's model. */
|
|
298
311
|
model?: string;
|
|
299
312
|
/** The effort the child runs on (gateway spelling); undefined = pi's default. */
|
|
300
313
|
effort?: string;
|
|
@@ -304,6 +317,21 @@ export interface ChildTask {
|
|
|
304
317
|
gatewayFallback?: { model: string; effort: string | null };
|
|
305
318
|
/** Extra environment for the child (the sub-agent setting and main model, for nested layers). */
|
|
306
319
|
env?: Record<string, string>;
|
|
320
|
+
/** The model[:effort] the delegating agent asked for (the task's own `model`), when it named one. */
|
|
321
|
+
requestedModel?: string;
|
|
322
|
+
/** That model did not run: the user's /subagents choice won, or the gateway does not serve it. */
|
|
323
|
+
override?: ChildModelOverride;
|
|
324
|
+
/** Where `model` comes from ("subagents", "user_prompt", "task", "parent"). */
|
|
325
|
+
modelSource?: string;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** A task model that did not run, and what ran instead (the result's model_override). */
|
|
329
|
+
export interface ChildModelOverride {
|
|
330
|
+
requested: string;
|
|
331
|
+
used: string;
|
|
332
|
+
effort: string | null;
|
|
333
|
+
reason: string;
|
|
334
|
+
note: string;
|
|
307
335
|
}
|
|
308
336
|
|
|
309
337
|
/** A sub-agent that ran on the main model instead of the picked one. */
|
|
@@ -330,6 +358,12 @@ export interface ChildResult {
|
|
|
330
358
|
effort?: string;
|
|
331
359
|
/** It ran on the main model instead of the picked one (why, and since when). */
|
|
332
360
|
model_fallback?: ChildModelFallback;
|
|
361
|
+
/** The task's own model did not run (the user's /subagents choice, or not served). */
|
|
362
|
+
model_override?: ChildModelOverride;
|
|
363
|
+
/** Where the model comes from: "subagents", "user_prompt", "task" or "parent". */
|
|
364
|
+
model_source?: string;
|
|
365
|
+
/** The model[:effort] the task asked for, when it named one. */
|
|
366
|
+
requested_model?: string;
|
|
333
367
|
status: ChildStatus;
|
|
334
368
|
/** Process exit code (null when killed by a signal or still unknown). */
|
|
335
369
|
exitCode: number | null;
|
|
@@ -400,6 +434,9 @@ export function buildChildResult(
|
|
|
400
434
|
...(task.model ? { model: task.model } : {}),
|
|
401
435
|
...(task.effort ? { effort: task.effort } : {}),
|
|
402
436
|
...(modelFallback ? { model_fallback: modelFallback } : {}),
|
|
437
|
+
...(task.override ? { model_override: { ...task.override } } : {}),
|
|
438
|
+
...(task.modelSource ? { model_source: task.modelSource } : {}),
|
|
439
|
+
...(task.requestedModel ? { requested_model: task.requestedModel } : {}),
|
|
403
440
|
status: input.status ?? (input.exitCode === 0 ? "completed" : "failed"),
|
|
404
441
|
exitCode: input.exitCode,
|
|
405
442
|
output: capped,
|
|
@@ -632,7 +669,7 @@ export interface ChildActivity {
|
|
|
632
669
|
}
|
|
633
670
|
|
|
634
671
|
/**
|
|
635
|
-
* The
|
|
672
|
+
* The session uploader reaches the running sub-agents through this global (it must
|
|
636
673
|
* stop them before its last capture; pi runs its session_shutdown handler
|
|
637
674
|
* before the agents extension's).
|
|
638
675
|
*/
|
|
@@ -909,16 +946,34 @@ export async function mapWithConcurrency<TIn, TOut>(
|
|
|
909
946
|
return results;
|
|
910
947
|
}
|
|
911
948
|
|
|
949
|
+
/** The model and effort a child really ran on (after a gateway fallback), and why that one. */
|
|
950
|
+
export function ranOn(result: Pick<ChildResult, "model" | "effort" | "model_fallback">): { model: string | null; effort: string | null } {
|
|
951
|
+
if (result.model_fallback?.kind === "gateway") return { model: result.model_fallback.used, effort: result.model_fallback.effort ?? null };
|
|
952
|
+
return { model: result.model ?? null, effort: result.effort ?? null };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function ranOnText(result: ChildResult): string {
|
|
956
|
+
const { model, effort } = ranOn(result);
|
|
957
|
+
if (!model) return "the parent's model (not an omnirush.ai model)";
|
|
958
|
+
const why = result.model_fallback?.kind === "gateway"
|
|
959
|
+
? "the gateway refused the chosen model"
|
|
960
|
+
: result.model_fallback
|
|
961
|
+
? "the main model"
|
|
962
|
+
: sourceNote(result.model_source);
|
|
963
|
+
return `${model}${effort ? ` · ${effort}` : ""}${why ? ` (${why})` : ""}`;
|
|
964
|
+
}
|
|
965
|
+
|
|
912
966
|
/** Structured result text for the parent model (one section per child). */
|
|
913
967
|
export function renderChildResults(results: ChildResult[], ids?: readonly string[]): string {
|
|
914
968
|
const succeeded = results.filter((result) => result.status === "completed").length;
|
|
915
969
|
const sections = results.map((result, index) => {
|
|
916
970
|
const minutes = Math.round((result.durationMs / 60_000) * 10) / 10;
|
|
917
|
-
const
|
|
918
|
-
const
|
|
919
|
-
const label = ranOn ? ` [${ranOn}${effort ? ` · ${effort}` : ""}]` : "";
|
|
971
|
+
const ran = ranOn(result);
|
|
972
|
+
const label = ran.model ? ` [${ran.model}${ran.effort ? ` · ${ran.effort}` : ""}]` : "";
|
|
920
973
|
const header = `### ${ids?.[index] ? `${ids[index]} ` : ""}${result.role}${label} — ${result.status} (${minutes} min${result.outputTruncated ? ", output capped" : ""})`;
|
|
921
974
|
const meta: string[] = [`task: ${result.task}`];
|
|
975
|
+
meta.push(`ran on: ${ranOnText(result)}`);
|
|
976
|
+
if (result.model_override) meta.push(`note: ${result.model_override.note}`);
|
|
922
977
|
if (result.model_fallback) meta.push(`note: ${result.model_fallback.note}`);
|
|
923
978
|
if (result.error) meta.push(`error: ${result.error}`);
|
|
924
979
|
return `${header}\n${meta.join("\n")}\n\n${result.output || "(no output)"}`;
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// <session dir>/subagents/ (children run without --no-session; that
|
|
28
28
|
// folder keeps them out of /resume and --continue)
|
|
29
29
|
// under a session id the parent picks, and the parent session's
|
|
30
|
-
//
|
|
30
|
+
// session uploader records each child's conversation as a sub-agent of the
|
|
31
31
|
// turn (session.child events with parent and depth, as the desktop
|
|
32
32
|
// records task sub-agents) — a background child that finishes in a later
|
|
33
33
|
// turn is recorded as it progresses, and one the session ends under is
|
|
@@ -61,6 +61,8 @@ import {
|
|
|
61
61
|
childInactivityMs,
|
|
62
62
|
renderAgentStatus,
|
|
63
63
|
subagentLayerNote,
|
|
64
|
+
SPAWN_GUIDELINE,
|
|
65
|
+
SUBAGENT_SPAWN_GUIDELINE,
|
|
64
66
|
renderChildResults,
|
|
65
67
|
renderDelivery,
|
|
66
68
|
runChildAgent,
|
|
@@ -71,6 +73,7 @@ import {
|
|
|
71
73
|
} from "./agents-lib";
|
|
72
74
|
import { SWARM_GUIDELINE } from "./swarm-lib";
|
|
73
75
|
import { omniDir } from "./auth";
|
|
76
|
+
import { loadCatalog, type CatalogModel } from "./subagents-lib";
|
|
74
77
|
import { MemoryAdmission, readMemorySettings } from "./memory-lib";
|
|
75
78
|
|
|
76
79
|
/** Blocking spawn_agents: progress updates at most this often (ms). */
|
|
@@ -82,16 +85,32 @@ const MIN_TIMEOUT_MINUTES = 1;
|
|
|
82
85
|
/** customType of the message that brings background results back. */
|
|
83
86
|
export const DELIVERY_TYPE = "omnirush-agents-result";
|
|
84
87
|
|
|
85
|
-
|
|
88
|
+
/** Served ids used as examples when the account's catalog is not cached (the live list at 0.10.3). */
|
|
89
|
+
export const SERVED_MODEL_EXAMPLES = ["gpt-6-astra", "gpt-6-sol", "gpt-5.6-sol", "meta-muse-spark", "muse-spark-1.1"];
|
|
90
|
+
|
|
91
|
+
/** Examples for the per-task model: the ids the account's live catalog lists (served models only). */
|
|
92
|
+
export function modelExamples(catalog: readonly CatalogModel[] | null): string[] {
|
|
93
|
+
if (!catalog || catalog.length === 0) return SERVED_MODEL_EXAMPLES;
|
|
94
|
+
return catalog.map((model) => model.id);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function modelParamDescription(examples: readonly string[]): string {
|
|
98
|
+
return [
|
|
99
|
+
"Optional model[:effort] for THIS task. Usually omit it: set it only when the user asked for a model for the sub-agents.",
|
|
100
|
+
`Served models: ${examples.map((id) => `"${id}"`).join(", ")}, optionally with an effort (e.g. "${examples[0] ?? "gpt-6-astra"}:high"); a model the account is not served is never used (the child runs on your model and effort).`,
|
|
101
|
+
"The user's /subagents choice always wins: while it is set, a model named here runs only if the user's own message names it.",
|
|
102
|
+
"Omitted, the child runs on the /subagents choice, else this session's current model and effort.",
|
|
103
|
+
].join(" ");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const spawnAgentsParams = (examples: readonly string[]) => Type.Object({
|
|
86
107
|
tasks: Type.Array(
|
|
87
108
|
Type.Object({
|
|
88
109
|
role: StringEnum(AGENT_ROLES, {
|
|
89
110
|
description: "code-searcher: read-only codebase exploration; researcher-web: web research via web_search/web_fetch; general-worker: full-tool implementation work",
|
|
90
111
|
}),
|
|
91
112
|
task: Type.String({ description: "Self-contained task description for the child agent (it sees ONLY this, not your conversation)" }),
|
|
92
|
-
model: Type.Optional(Type.String({
|
|
93
|
-
description: 'Optional model[:effort] for THIS task (e.g. "muse-spark-1.3", "muse-spark-1.2-contributor" for cheap swarm workers, "meta-muse-spark", "muse-spark-1.1:low", "gpt-6-astra", "gpt-6-sol:high", "gpt-5.6-sol"). Omit (or leave empty) for the sub-agent model the user picked with /subagents, else the parent session\'s current model and effort',
|
|
94
|
-
})),
|
|
113
|
+
model: Type.Optional(Type.String({ description: modelParamDescription(examples) })),
|
|
95
114
|
}),
|
|
96
115
|
{ description: "Tasks to delegate; they all run in parallel", minItems: 1 },
|
|
97
116
|
),
|
|
@@ -113,6 +132,26 @@ const IdsParam = Type.Optional(Type.Array(Type.String(), {
|
|
|
113
132
|
description: 'Agent ids from spawn_agents (e.g. "a3"); omit (or "all") for every agent of this session',
|
|
114
133
|
}));
|
|
115
134
|
|
|
135
|
+
/** The text of the latest user message on the session's branch (null when there is none). */
|
|
136
|
+
export function lastUserMessageText(ctx: any): string | null {
|
|
137
|
+
let entries: any[] = [];
|
|
138
|
+
try {
|
|
139
|
+
entries = ctx?.sessionManager?.getBranch?.() ?? ctx?.sessionManager?.getEntries?.() ?? [];
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
144
|
+
const message = entries[index]?.type === "message" ? entries[index].message : null;
|
|
145
|
+
if (!message || message.role !== "user") continue;
|
|
146
|
+
if (typeof message.content === "string") return message.content;
|
|
147
|
+
if (Array.isArray(message.content)) {
|
|
148
|
+
return message.content.filter((part: any) => part?.type === "text" && typeof part.text === "string").map((part: any) => part.text).join("\n");
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
116
155
|
function resultOf(record: AgentRecord): ChildResult & { agent_id: string } {
|
|
117
156
|
return { ...(record.result as ChildResult), agent_id: record.id };
|
|
118
157
|
}
|
|
@@ -225,6 +264,30 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
225
264
|
};
|
|
226
265
|
(globalThis as any)[AGENTS_HOOK] = hook;
|
|
227
266
|
|
|
267
|
+
// The user's latest message (the text they typed, not an extension's):
|
|
268
|
+
// a task's own model runs over the /subagents choice only when it names
|
|
269
|
+
// that model. Sub-agents have no user: their task text never counts.
|
|
270
|
+
let lastUserInput: { session: string; text: string } | null = null;
|
|
271
|
+
const sessionIdOf = (ctx: any): string => {
|
|
272
|
+
try {
|
|
273
|
+
return String(ctx?.sessionManager?.getSessionId?.() ?? "");
|
|
274
|
+
} catch {
|
|
275
|
+
return "";
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
if (depth === 0) {
|
|
279
|
+
pi.on("input", (event: any, ctx: any) => {
|
|
280
|
+
if (event?.source === "extension" || typeof event?.text !== "string") return;
|
|
281
|
+
lastUserInput = { session: sessionIdOf(ctx), text: event.text };
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const latestUserPrompt = (ctx: any): string | null => {
|
|
285
|
+
if (depth > 0) return null;
|
|
286
|
+
const session = sessionIdOf(ctx);
|
|
287
|
+
if (lastUserInput && lastUserInput.session === session) return lastUserInput.text;
|
|
288
|
+
return lastUserMessageText(ctx);
|
|
289
|
+
};
|
|
290
|
+
|
|
228
291
|
const sessionOf = (ctx: any): string => {
|
|
229
292
|
lastContext = ctx ?? lastContext;
|
|
230
293
|
return hub.sessionOf(ctx);
|
|
@@ -242,19 +305,20 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
242
305
|
"Default (wait true) blocks until all children finish and returns their results.",
|
|
243
306
|
"wait:false dispatches them in the BACKGROUND: the call returns at once with agent ids, you keep working, and their results arrive automatically as a message in this conversation when they finish.",
|
|
244
307
|
"Manage background agents with agents_status, agents_wait, agents_result and agents_cancel.",
|
|
245
|
-
'Each task can run on a
|
|
308
|
+
'Each task can run on a different model via its "model" field when the user asks for one; the user\'s /subagents choice wins over it, and a model the account is not served is not used. Every result says the model and effort the child ran on.',
|
|
246
309
|
"Every task description must be SELF-CONTAINED: the child cannot see this conversation.",
|
|
247
310
|
"Use for parallelizable work: broad code surveys, independent research questions, independent implementation chunks.",
|
|
311
|
+
"Give each task only its own part of the work: never the user's whole message or instructions about sub-agents (the child would follow them and delegate again).",
|
|
248
312
|
].join(" "),
|
|
249
313
|
promptSnippet: "spawn_agents: delegate parallel tasks to role-preset subagents (blocking, or wait:false for background work)",
|
|
250
314
|
promptGuidelines: [
|
|
251
|
-
|
|
315
|
+
depth > 0 ? SUBAGENT_SPAWN_GUIDELINE : SPAWN_GUIDELINE,
|
|
252
316
|
"Short fan-outs whose results you need right away (quick searches, lookups): the default blocking call.",
|
|
253
317
|
"Long work (implementation chunks, builds, test suites, long research — anything that may take many minutes) while you have other things to do: spawn_agents with wait:false, then keep working; the results arrive on their own as a message, so do not poll agents_status in a loop. Call agents_wait only when you have nothing else to do and need the results before going on.",
|
|
254
318
|
"Do not use spawn_agents for a single quick action — doing it yourself is cheaper.",
|
|
255
|
-
SWARM_GUIDELINE,
|
|
319
|
+
...(depth > 0 ? [] : [SWARM_GUIDELINE]),
|
|
256
320
|
],
|
|
257
|
-
parameters:
|
|
321
|
+
parameters: spawnAgentsParams(modelExamples(loadCatalog(omniDir()))),
|
|
258
322
|
|
|
259
323
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
260
324
|
const parentSessionId = sessionOf(ctx);
|
|
@@ -263,9 +327,9 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
263
327
|
if (!raw || typeof raw.task !== "string" || !raw.task.trim()) {
|
|
264
328
|
throw new Error("invalid tasks: every entry needs a non-empty task string");
|
|
265
329
|
}
|
|
266
|
-
// A task's own model[:effort]
|
|
267
|
-
// the /subagents
|
|
268
|
-
//
|
|
330
|
+
// A task's own model[:effort] (resolved below by subagentTasks):
|
|
331
|
+
// the user's /subagents choice wins over it unless the user's message
|
|
332
|
+
// names that model; unset, it runs only when the gateway serves it.
|
|
269
333
|
const requestedModel = typeof raw.model === "string" ? raw.model.trim() : "";
|
|
270
334
|
tasks.push({
|
|
271
335
|
role: raw.role,
|
|
@@ -274,11 +338,11 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
274
338
|
});
|
|
275
339
|
}
|
|
276
340
|
if (tasks.length === 0) throw new Error("no tasks given");
|
|
277
|
-
// The model and effort each child runs on (subagents.ts): the
|
|
278
|
-
//
|
|
341
|
+
// The model and effort each child runs on (subagents.ts): the
|
|
342
|
+
// /subagents choice, else a served task model, else the parent
|
|
279
343
|
// session's current model and effort; the main model when a pick is
|
|
280
|
-
// unavailable.
|
|
281
|
-
const resolvedTasks = subagentTasks(pi, ctx, tasks);
|
|
344
|
+
// unavailable. Overrides are noted on the result and the trace.
|
|
345
|
+
const resolvedTasks = subagentTasks(pi, ctx, tasks, { userPrompt: latestUserPrompt(ctx) });
|
|
282
346
|
if (!parentSessionId) throw new Error("no parent session id — subagents cannot be traced");
|
|
283
347
|
const workdir = ctx?.cwd ? String(ctx.cwd) : process.cwd();
|
|
284
348
|
// Only a deliberate limit counts: models fill optional numbers with
|
|
@@ -331,7 +395,7 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
331
395
|
|
|
332
396
|
if (background) {
|
|
333
397
|
const lines = dispatched.records.map((record, index) => {
|
|
334
|
-
const note = childModelFallback(resolvedTasks[index])?.note;
|
|
398
|
+
const note = [resolvedTasks[index].override?.note, childModelFallback(resolvedTasks[index])?.note].filter(Boolean).join("; ");
|
|
335
399
|
return `- ${record.id}: ${record.role}${record.model ? ` [${record.model}${record.effort ? ` · ${record.effort}` : ""}]` : ""} — ${record.task.length > 100 ? `${record.task.slice(0, 99)}…` : record.task}${note ? ` (${note})` : ""}`;
|
|
336
400
|
});
|
|
337
401
|
return {
|
|
@@ -360,6 +424,9 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
360
424
|
...(record.model ? { model: record.model } : {}),
|
|
361
425
|
...(record.effort ? { effort: record.effort } : {}),
|
|
362
426
|
...(modelFallback ? { model_fallback: modelFallback } : {}),
|
|
427
|
+
...(resolvedTasks[index].override ? { model_override: { ...resolvedTasks[index].override } } : {}),
|
|
428
|
+
...(resolvedTasks[index].modelSource ? { model_source: resolvedTasks[index].modelSource } : {}),
|
|
429
|
+
...(resolvedTasks[index].requestedModel ? { requested_model: resolvedTasks[index].requestedModel } : {}),
|
|
363
430
|
parallel_limit: record.parallelLimit,
|
|
364
431
|
status: "running",
|
|
365
432
|
};
|
|
@@ -474,7 +541,7 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
474
541
|
});
|
|
475
542
|
|
|
476
543
|
pi.on("session_shutdown", async () => {
|
|
477
|
-
// Children the session ends under are stopped (the
|
|
544
|
+
// Children the session ends under are stopped (the session uploader, when it
|
|
478
545
|
// runs, already did this before its last capture).
|
|
479
546
|
await manager.interruptAll().catch(() => undefined);
|
|
480
547
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Omnirush device-flow auth core — shared by the CLI (src/bin.js), the
|
|
2
|
-
// extensions (sota guard 401-refresh,
|
|
2
|
+
// extensions (sota guard 401-refresh, session uploader), and node:test.
|
|
3
3
|
//
|
|
4
4
|
// Plain ESM JavaScript on node builtins only: pi extensions are loaded
|
|
5
5
|
// through jiti with a fixed virtual-module set (pi packages + typebox),
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
// -> 409 {"detail":"refresh_token_already_used"} (race)
|
|
24
24
|
// -> 403 {"detail":"account_inactive|account_pending|account_rejected"}
|
|
25
25
|
// GET /device/me -> 200 device identity | 401 device_token_invalid
|
|
26
|
-
// POST
|
|
26
|
+
// POST <upload endpoint> -> zstd body, X-Omnirush-Session-ID header
|
|
27
|
+
// (UPLOAD_ENDPOINT_PATH, capture/workspace-sync.ts)
|
|
27
28
|
//
|
|
28
29
|
// SECURITY: nothing in this module ever returns or logs a token; error
|
|
29
30
|
// messages carry only server `detail` strings and HTTP statuses.
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
tailText,
|
|
42
42
|
} from "./bgshell-lib";
|
|
43
43
|
import { deliveryHub } from "./deliveries";
|
|
44
|
-
import { BACKGROUND_BASH_TYPE } from "./
|
|
44
|
+
import { BACKGROUND_BASH_TYPE } from "./engine-messages";
|
|
45
45
|
import { sanitizeToolEnvironment } from "./secret-env";
|
|
46
46
|
|
|
47
47
|
/** customType of the message that brings finished background commands back (the trace maps it to bash tool parts). */
|
|
@@ -78,14 +78,14 @@ export default function (pi: any, options: { exec?: ShellExec; now?: () => numbe
|
|
|
78
78
|
const fgDefault = defaultForegroundSeconds();
|
|
79
79
|
|
|
80
80
|
/** New output of each job, marked read. */
|
|
81
|
-
const
|
|
81
|
+
const readAll = (jobs: ShellJob[]) => jobs.map((job) => ({ job, ...manager.read(job) }));
|
|
82
82
|
|
|
83
83
|
hub.addSource({
|
|
84
84
|
hasPending: (session) => manager.hasPending(session),
|
|
85
85
|
take: (session) => {
|
|
86
86
|
const jobs = manager.takeDeliverable(session);
|
|
87
87
|
if (jobs.length === 0) return [];
|
|
88
|
-
const parts =
|
|
88
|
+
const parts = readAll(jobs);
|
|
89
89
|
return [{
|
|
90
90
|
customType: BASH_DELIVERY_TYPE,
|
|
91
91
|
content: renderJobDelivery(parts),
|
|
@@ -115,7 +115,7 @@ export default function (pi: any, options: { exec?: ShellExec; now?: () => numbe
|
|
|
115
115
|
|
|
116
116
|
// --- stopping at the end of the session -------------------------------------
|
|
117
117
|
|
|
118
|
-
/** What the session's end stopped (the
|
|
118
|
+
/** What the session's end stopped (the session uploader's hook may do it before our own handler runs). */
|
|
119
119
|
let stoppedAtEnd: ShellJob[] = [];
|
|
120
120
|
const stopAll = async (session?: string): Promise<ShellJob[]> => {
|
|
121
121
|
const live = manager.running(session);
|
|
@@ -127,7 +127,7 @@ export default function (pi: any, options: { exec?: ShellExec; now?: () => numbe
|
|
|
127
127
|
const background = stopped.filter((job) => job.background);
|
|
128
128
|
if (background.length > 0) {
|
|
129
129
|
try {
|
|
130
|
-
const parts =
|
|
130
|
+
const parts = readAll(background);
|
|
131
131
|
pi.sendMessage(
|
|
132
132
|
{
|
|
133
133
|
customType: BASH_DELIVERY_TYPE,
|
|
@@ -296,7 +296,7 @@ export default function (pi: any, options: { exec?: ShellExec; now?: () => numbe
|
|
|
296
296
|
}));
|
|
297
297
|
|
|
298
298
|
const readResult = (jobs: ShellJob[], unknown: string[]) => {
|
|
299
|
-
const parts =
|
|
299
|
+
const parts = readAll(jobs);
|
|
300
300
|
const sections = parts.map(({ job, text, dropped }) => renderJobResult(job, text, { dropped }).text);
|
|
301
301
|
if (unknown.length) sections.push(`Unknown ids: ${unknown.join(", ")}`);
|
|
302
302
|
return {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Ported from omnirush-ai/omnirush-gui @ 2e166a9f9843dccbedc8277addcd94b752e66adb
|
|
2
2
|
(apps/server/src), unchanged except for the CLI seams marked "CLI:" in the code:
|
|
3
3
|
|
|
4
|
-
workspace-
|
|
4
|
+
workspace-sync.ts vendored minimatch import; exported ledger reader and
|
|
5
5
|
.gitignore helpers; `watch: false` (poll on snapshots)
|
|
6
6
|
and `envelopeMetadata` options
|
|
7
7
|
turn-diff.ts unchanged
|
|
8
|
-
|
|
8
|
+
sync-upload-budget.ts unchanged
|
|
9
9
|
session-archive/detect.ts, files.ts, lifecycle.ts, pack.ts, seal.ts unchanged
|
|
10
10
|
session-archive/index.ts, policy.ts, upload.ts vendored zod import
|
|
11
11
|
session-archive/manifest.ts, touched.ts gitignored content left out
|
|
@@ -14,8 +14,11 @@ Ported from omnirush-ai/omnirush-gui @ 2e166a9f9843dccbedc8277addcd94b752e66adb
|
|
|
14
14
|
CLI files beside them:
|
|
15
15
|
session-archive/ignored.ts git's ignore rules for the archive scans
|
|
16
16
|
server-fetch.ts external egress = the global fetch
|
|
17
|
-
omnirush-swarm.ts the one constant
|
|
17
|
+
omnirush-swarm.ts the one constant workspace-sync.ts reads
|
|
18
18
|
vendor/ zod and minimatch (npm run build:capture-vendor)
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
The CLI names some modules and exports differently from upstream
|
|
21
|
+
(dev/gui-parity-tests.sh maps them back for the desktop's test suites).
|
|
22
|
+
|
|
23
|
+
Resync: copy the files again, re-apply the seams and the CLI names, then run
|
|
21
24
|
dev/gui-parity-tests.sh <omnirush-gui checkout>.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// The one constant the ported workspace
|
|
1
|
+
// The one constant the ported workspace uploader reads from the desktop's
|
|
2
2
|
// swarm module (apps/server/src/omnirush-swarm.ts): the trace event that
|
|
3
3
|
// records a sub-agent answered on the main model instead of the picked one.
|
|
4
4
|
// The CLI never emits it (its sub-agents run in their own process on the
|
|
5
|
-
// model they were given), but the
|
|
5
|
+
// model they were given), but the session uploader keeps the same handling.
|
|
6
6
|
export const SUBAGENT_MODEL_FALLBACK_TRACE = "subagent.model_fallback";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Egress seam of the ported capture modules (desktop: apps/server/src/
|
|
2
2
|
// server-fetch.ts). The desktop routes external egress through the embedding
|
|
3
3
|
// runtime's network stack; the CLI has one runtime and one fetch, so every
|
|
4
|
-
// upload (
|
|
4
|
+
// upload (session upload envelopes, archive API calls, presigned S3 part PUTs) goes
|
|
5
5
|
// through the global fetch.
|
|
6
6
|
export function externalFetch(input: string, init?: RequestInit): Promise<Response> {
|
|
7
7
|
return globalThis.fetch(input, init);
|
|
@@ -12,7 +12,7 @@ import { lstat, open, realpath } from "node:fs/promises";
|
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import path, { isAbsolute, join, parse, posix, relative, resolve, sep, win32 } from "node:path";
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { isSyncDirectoryDenied } from "../workspace-sync.js";
|
|
16
16
|
import type { ArchivePolicy } from "./policy.js";
|
|
17
17
|
|
|
18
18
|
export type ArchivableProject = {
|
|
@@ -291,7 +291,7 @@ export function foldGatePath(path: string, platform: NodeJS.Platform): string {
|
|
|
291
291
|
* - the Electron userData directory, and anything above it;
|
|
292
292
|
* - a credential store wherever it is (`.ssh`, `.aws`, `.gnupg`, `.kube`,
|
|
293
293
|
* `.docker`, `.azure`, `.password-store`, `Keychains`, `.config/gcloud`),
|
|
294
|
-
* and any folder the
|
|
294
|
+
* and any folder the session uploader's denylist denies as a whole (`keys`,
|
|
295
295
|
* `secrets`, `credentials*`, `.env*`, `node_modules`, `.git`, ...);
|
|
296
296
|
* - app data wherever it is (`AppData`, `Library/Application Support`);
|
|
297
297
|
* - in the home directory, and in the other folders beside it (other
|
|
@@ -317,7 +317,7 @@ export function refusedFolderRoot(root: string, context: FolderGateContext): Fol
|
|
|
317
317
|
|
|
318
318
|
const names = target.slice(paths.parse(target).root.length).split(paths.sep).filter(Boolean).map((name) => name.toLowerCase());
|
|
319
319
|
const hasPair = (pairs: ReadonlyArray<readonly [string, string]>) => names.some((name, index) => pairs.some(([first, second]) => name === first && names[index + 1] === second));
|
|
320
|
-
if (names.some((name) => CREDENTIAL_DIRS.has(name)) || hasPair(CREDENTIAL_DIR_PAIRS) ||
|
|
320
|
+
if (names.some((name) => CREDENTIAL_DIRS.has(name)) || hasPair(CREDENTIAL_DIR_PAIRS) || isSyncDirectoryDenied(names.join("/"))) return "root_credentials";
|
|
321
321
|
if (names.some((name) => APP_DATA_DIRS.has(name)) || hasPair(APP_DATA_DIR_PAIRS)) return "root_app_data";
|
|
322
322
|
|
|
323
323
|
for (const home of homes) {
|
|
@@ -72,7 +72,7 @@ export async function readJsonFile(path: string): Promise<unknown> {
|
|
|
72
72
|
/**
|
|
73
73
|
* Runs a full garbage collection when the runtime offers one (Bun.gc); a
|
|
74
74
|
* no-op elsewhere. Streaming an archive churns through hundreds of MiB of
|
|
75
|
-
* short-lived buffers (zstd output, AES-GCM output) that the
|
|
75
|
+
* short-lived buffers (zstd output, AES-GCM output) that the session uploader would
|
|
76
76
|
* otherwise let pile up well past the stream's bounded working set. Callers
|
|
77
77
|
* hint once per 64 MiB of data; a collection takes a few milliseconds at the
|
|
78
78
|
* heap sizes involved.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* ignored folder listed once as `dir/` so the scan prunes it without
|
|
15
15
|
* walking it. Where git cannot answer (git missing, not a work tree, a
|
|
16
16
|
* timeout or an output past the cap) the folder's `.gitignore` files are
|
|
17
|
-
* applied level by level, with the same matcher the workspace
|
|
17
|
+
* applied level by level, with the same matcher the workspace uploader's
|
|
18
18
|
* fallback listing uses (`ignoredByRules`).
|
|
19
19
|
*
|
|
20
20
|
* This file is a CLI addition to the ported desktop modules; the desktop's
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { join } from "node:path";
|
|
24
24
|
|
|
25
|
-
import { ignoredByRules, readGitignoreFile, scopedIgnoreRules } from "../workspace-
|
|
25
|
+
import { ignoredByRules, readGitignoreFile, scopedIgnoreRules } from "../workspace-sync.js";
|
|
26
26
|
import { runGit } from "./manifest.js";
|
|
27
27
|
|
|
28
28
|
/** The ignored-paths listing is read up to this size; past it the .gitignore fallback applies. */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* with the touched-files policy on, the same chain holding only the files
|
|
9
9
|
* the agent touched there (touched.ts), its base at the first capture that
|
|
10
10
|
* has one. Each archive is sealed to the omnirush.ai archive key, queued
|
|
11
|
-
* durably under the
|
|
11
|
+
* durably under the state dir and uploaded to S3 through
|
|
12
12
|
* presigned multipart URLs. The embedded server drives it through
|
|
13
13
|
* lifecycle.ts; see README.md.
|
|
14
14
|
*/
|
|
@@ -68,7 +68,7 @@ export { gitMarkerDetector, gitParentDetector, isArchivableProject, type Archiva
|
|
|
68
68
|
export { isArchiveCredentialPath, type ArchiveTrigger, type FinalReason } from "./manifest.js";
|
|
69
69
|
export type { ArchiveApiRequest } from "./upload.js";
|
|
70
70
|
|
|
71
|
-
/** Subdirectory of the
|
|
71
|
+
/** Subdirectory of the state dir that holds everything the archiver keeps. */
|
|
72
72
|
export const ARCHIVE_STATE_DIRECTORY = "omnirush-archive";
|
|
73
73
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9._:-]{8,128}$/;
|
|
74
74
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
@@ -88,16 +88,16 @@ const SIGN_OUT_ABORT_TIMEOUT_MS = 5_000;
|
|
|
88
88
|
export const POLICY_TTL_MS = 5 * 60_000;
|
|
89
89
|
|
|
90
90
|
export type SessionArchiverOptions = {
|
|
91
|
-
/** As the
|
|
91
|
+
/** As the session uploader: the archive routes are derived from it like the upload URL. */
|
|
92
92
|
gatewayUrl?: string;
|
|
93
93
|
accessToken?: string;
|
|
94
|
-
/** The
|
|
94
|
+
/** The session uploader's hook: a fresh bearer after a 401, or null. */
|
|
95
95
|
refreshAccessToken?: () => Promise<string | null>;
|
|
96
96
|
/** External egress for S3 part PUTs (and API calls without `request`); externalFetch by default. */
|
|
97
97
|
fetch?: ArchiveFetch;
|
|
98
98
|
/** Authenticated API calls through the device-session owner (the gateway broker); replaces gatewayUrl + accessToken. */
|
|
99
99
|
request?: ArchiveApiRequest;
|
|
100
|
-
/** The
|
|
100
|
+
/** The state dir; the archiver keeps its files in `<stateDir>/omnirush-archive/`. */
|
|
101
101
|
stateDir: string;
|
|
102
102
|
/** App state/temp/data dirs pruned when under a root (the state dir itself is always pruned). */
|
|
103
103
|
excludedDirs?: string[];
|
|
@@ -358,7 +358,7 @@ export class SessionArchiver {
|
|
|
358
358
|
return this.started;
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
/** The device bearer changed (
|
|
361
|
+
/** The device bearer changed (uploader token rotation). */
|
|
362
362
|
setAccessToken(token: string | null): void {
|
|
363
363
|
this.uploader.setAccessToken(token);
|
|
364
364
|
}
|
|
@@ -491,7 +491,7 @@ export class SessionArchiver {
|
|
|
491
491
|
|
|
492
492
|
/**
|
|
493
493
|
* A path the session touched, workspace-relative (portable `/`), as the
|
|
494
|
-
*
|
|
494
|
+
* session uploader reports it: a tool's path in the trace, or a change the
|
|
495
495
|
* watcher saw. Kept (on disk, a moment later) for a touched-files session;
|
|
496
496
|
* dropped for any other once the gate has run. Cheap: called for every
|
|
497
497
|
* file event.
|
|
@@ -827,7 +827,7 @@ export class SessionArchiver {
|
|
|
827
827
|
}
|
|
828
828
|
|
|
829
829
|
/**
|
|
830
|
-
* One capture under the session's lock. The collection hints around it keep
|
|
830
|
+
* One capture under the session's lock. The garbage collection hints around it keep
|
|
831
831
|
* a capture's per-entry garbage from stacking on top of the previous one's.
|
|
832
832
|
*/
|
|
833
833
|
private async captureArchive(state: SessionState, kind: ArchiveKind, turn: number, key: ArchiveKey, generation: number, options: CaptureOptions = {}): Promise<CaptureResult> {
|
|
@@ -272,7 +272,7 @@ export class ProjectArchiveLifecycle {
|
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
/**
|
|
275
|
-
* The
|
|
275
|
+
* The session uploader saw the session touch a path (workspace-relative): a
|
|
276
276
|
* tool's path in the trace, or a change on disk. Kept for a touched-files
|
|
277
277
|
* session (the archiver drops it for any other); nothing for a session
|
|
278
278
|
* this app run has not started or knows is not archived.
|
|
@@ -12,7 +12,7 @@ import { lstat, open, readdir, readlink, realpath } from "node:fs/promises";
|
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { basename, delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { clampSyncBytes, isSyncPathDenied, stripRemoteUserinfo } from "../workspace-sync.js";
|
|
16
16
|
import { hintGarbageCollection } from "./files.js";
|
|
17
17
|
|
|
18
18
|
export const ARCHIVE_SCHEMA = "omnirush.archive.v1";
|
|
@@ -143,19 +143,19 @@ export function compareArchivePaths(left: string, right: string): number {
|
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
145
|
* True when a file or symlink must be left out of the archive (section 5.3):
|
|
146
|
-
* the
|
|
146
|
+
* the session uploader's credential denylist, with git internals exempt and
|
|
147
147
|
* `node_modules` not counted as a denial.
|
|
148
148
|
*/
|
|
149
149
|
export function isArchiveCredentialPath(relPath: string): boolean {
|
|
150
150
|
const parts = relPath.split("/");
|
|
151
151
|
if (parts.some((part) => part.toLowerCase() === ".git")) return false;
|
|
152
152
|
const rest = parts.filter((part) => part.toLowerCase() !== "node_modules");
|
|
153
|
-
return rest.length > 0 &&
|
|
153
|
+
return rest.length > 0 && isSyncPathDenied(rest.join("/"));
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/** workspace.label: the root's basename, at most 255 UTF-8 bytes. */
|
|
157
157
|
export function archiveLabel(root: string): string {
|
|
158
|
-
return
|
|
158
|
+
return clampSyncBytes(basename(resolve(root)), MAX_LABEL_BYTES).text;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
// --- hash cache -------------------------------------------------------------------
|