omnirush 0.7.0 → 0.8.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/collect-once.ts +72 -141
- package/assets/extensions/omnirush/agents-lib.ts +23 -13
- package/assets/extensions/omnirush/agents.ts +19 -24
- package/assets/extensions/omnirush/capture/UPSTREAM +21 -0
- package/assets/extensions/omnirush/capture/collect-upload-budget.ts +39 -0
- package/assets/extensions/omnirush/capture/omnirush-swarm.ts +6 -0
- package/assets/extensions/omnirush/capture/server-fetch.ts +8 -0
- package/assets/extensions/omnirush/capture/session-archive/detect.ts +410 -0
- package/assets/extensions/omnirush/capture/session-archive/files.ts +85 -0
- package/assets/extensions/omnirush/capture/session-archive/ignored.ts +123 -0
- package/assets/extensions/omnirush/capture/session-archive/index.ts +1468 -0
- package/assets/extensions/omnirush/capture/session-archive/lifecycle.ts +606 -0
- package/assets/extensions/omnirush/capture/session-archive/manifest.ts +827 -0
- package/assets/extensions/omnirush/capture/session-archive/pack.ts +481 -0
- package/assets/extensions/omnirush/capture/session-archive/policy.ts +21 -0
- package/assets/extensions/omnirush/capture/session-archive/seal.ts +481 -0
- package/assets/extensions/omnirush/capture/session-archive/touched.ts +550 -0
- package/assets/extensions/omnirush/capture/session-archive/upload.ts +601 -0
- package/assets/extensions/omnirush/capture/turn-diff.ts +661 -0
- package/assets/extensions/omnirush/capture/vendor/minimatch.js +4 -0
- package/assets/extensions/omnirush/capture/vendor/zod.js +60 -0
- package/assets/extensions/omnirush/capture/workspace-collector.ts +5021 -0
- package/assets/extensions/omnirush/collector.ts +539 -171
- package/assets/extensions/omnirush/commands.ts +1 -1
- package/assets/extensions/omnirush/pi-engine.ts +655 -0
- package/assets/extensions/omnirush/plan.ts +6 -7
- package/package.json +5 -2
- package/scripts/build-capture-vendor.js +55 -0
- package/src/bin.js +30 -5
- package/assets/extensions/omnirush/collector-lib.ts +0 -2433
- package/assets/extensions/omnirush/trace-format.ts +0 -311
package/assets/collect-once.ts
CHANGED
|
@@ -2,174 +2,105 @@
|
|
|
2
2
|
// trigger; the same snapshots also flow automatically during agent
|
|
3
3
|
// sessions via the omnirush pi extension).
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// Runs the session collector (the desktop app's, see
|
|
6
|
+
// extensions/omnirush/capture/) for one manual session: the start
|
|
7
|
+
// snapshot of the folder (files[] within the snapshot cap, the full
|
|
8
|
+
// manifest, the git block; gitignored and denied paths left out, content
|
|
9
|
+
// scrubbed) and the session's end snapshot, each one zstd envelope POSTed
|
|
10
|
+
// to {origin}/collect with the device access token. A retryable failure
|
|
11
|
+
// stays in the collector's spool and is sent by the next CLI run.
|
|
9
12
|
//
|
|
10
|
-
//
|
|
11
|
-
// each part is discarded right after its upload — nothing workspace-
|
|
12
|
-
// sized is ever held in full. The pre-flight summary walks sizes only
|
|
13
|
-
// (lstat), never content. Deterministic order: the walker yields paths
|
|
14
|
-
// sorted, each path's entries (manifest + slices) grouped.
|
|
15
|
-
//
|
|
16
|
-
// Usage: omnirush collect [--dir PATH] [--yes] [--dry-run]
|
|
13
|
+
// Usage: omnirush collect [--dir PATH] [--dry-run]
|
|
17
14
|
|
|
18
|
-
import { randomBytes } from "node:crypto";
|
|
15
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
19
16
|
import path from "node:path";
|
|
20
|
-
import
|
|
17
|
+
import { zstdDecompressSync } from "node:zlib";
|
|
21
18
|
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
iterateEnvelopeParts,
|
|
26
|
-
MAX_PART_COMPRESSED_BYTES,
|
|
27
|
-
MAX_PART_COMPRESSED_HARD,
|
|
28
|
-
MAX_SESSION_BYTES,
|
|
29
|
-
} from "./extensions/omnirush/collector-lib";
|
|
19
|
+
import { gatewayUrlForOrigin, omniDir, resolveOrigin } from "./extensions/omnirush/auth";
|
|
20
|
+
import { WorkspaceCollector } from "./extensions/omnirush/capture/workspace-collector";
|
|
21
|
+
import { sharedRefresher } from "./extensions/omnirush/refresh";
|
|
30
22
|
|
|
31
|
-
function parseArgs(argv: string[]): { dir: string;
|
|
23
|
+
function parseArgs(argv: string[]): { dir: string; dryRun: boolean } {
|
|
32
24
|
let dir = process.cwd();
|
|
33
|
-
let yes = false;
|
|
34
25
|
let dryRun = false;
|
|
35
26
|
for (let i = 0; i < argv.length; i++) {
|
|
36
27
|
const arg = argv[i];
|
|
37
28
|
if (arg === "--dir" && argv[i + 1]) {
|
|
38
29
|
dir = path.resolve(argv[++i]);
|
|
39
|
-
} else if (arg === "--yes") {
|
|
40
|
-
yes = true;
|
|
41
30
|
} else if (arg === "--dry-run") {
|
|
42
31
|
dryRun = true;
|
|
32
|
+
} else if (arg === "--yes") {
|
|
33
|
+
// Accepted for compatibility: the upload never asks.
|
|
43
34
|
} else if (arg === "-h" || arg === "--help") {
|
|
44
|
-
console.log("usage: omnirush collect [--dir PATH] [--
|
|
35
|
+
console.log("usage: omnirush collect [--dir PATH] [--dry-run]");
|
|
45
36
|
process.exit(0);
|
|
46
37
|
}
|
|
47
38
|
}
|
|
48
|
-
return { dir,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function confirm(question: string): Promise<boolean> {
|
|
52
|
-
if (!process.stdin.isTTY) return false;
|
|
53
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
54
|
-
const answer = await new Promise<string>((resolve) => rl.question(question, resolve));
|
|
55
|
-
rl.close();
|
|
56
|
-
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Cheap workspace summary: path count + on-disk bytes (no content). */
|
|
60
|
-
async function summarize(dir: string): Promise<{ files: number; bytes: number }> {
|
|
61
|
-
const { lstat } = await import("node:fs/promises");
|
|
62
|
-
const { listWorkspaceFiles } = await import("./extensions/omnirush/collector-lib");
|
|
63
|
-
const paths = await listWorkspaceFiles(dir);
|
|
64
|
-
let bytes = 0;
|
|
65
|
-
for (const relativePath of paths) {
|
|
66
|
-
try {
|
|
67
|
-
bytes += (await lstat(path.join(dir, relativePath))).size;
|
|
68
|
-
} catch {
|
|
69
|
-
/* raced away — the streaming pass re-checks */
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return { files: paths.length, bytes };
|
|
39
|
+
return { dir, dryRun };
|
|
73
40
|
}
|
|
74
41
|
|
|
75
42
|
async function main() {
|
|
76
|
-
const { dir,
|
|
43
|
+
const { dir, dryRun } = parseArgs(process.argv.slice(2));
|
|
77
44
|
const sessionId = `manual-${randomBytes(6).toString("hex")}`;
|
|
78
|
-
const workspaceId = path.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (summary.files === 0) {
|
|
82
|
-
console.error("omnirush collect: nothing to upload (empty or fully filtered workspace)");
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
console.log(
|
|
86
|
-
`collect: ${summary.files} files (${summary.bytes} bytes on disk, denylist applied) as session ${sessionId}`,
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
const origin = resolveOrigin(process.env);
|
|
90
|
-
const collectUrl = gatewayUrlForOrigin(origin).replace(/\/v1$/, "/collect");
|
|
91
|
-
const accessToken = (
|
|
92
|
-
process.env.OMNIRUSH_ACCESS_TOKEN ||
|
|
93
|
-
process.env.OMNIRUSH_TOKEN ||
|
|
94
|
-
""
|
|
95
|
-
).trim();
|
|
96
|
-
if (!accessToken) {
|
|
45
|
+
const workspaceId = `cli_${createHash("sha256").update(path.resolve(dir)).digest("hex").slice(0, 16)}`;
|
|
46
|
+
const accessToken = (process.env.OMNIRUSH_ACCESS_TOKEN || process.env.OMNIRUSH_TOKEN || "").trim();
|
|
47
|
+
if (!accessToken && !dryRun) {
|
|
97
48
|
console.error("omnirush collect: no access token (run `omnirush login`)");
|
|
98
49
|
process.exit(1);
|
|
99
50
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
},
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.map(([name, value]) => `${name}: ${value}`);
|
|
150
|
-
const sequence = JSON.parse(part.payload.toString("utf8")).sequence;
|
|
151
|
-
console.log(
|
|
152
|
-
`uploaded part sequence=${sequence} (${part.compressed.length} bytes, HTTP ${response.status})` +
|
|
153
|
-
`${extra.length ? "\n " + extra.join("\n ") : ""}`,
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (partCount === 0) {
|
|
158
|
-
console.error("omnirush collect: nothing to upload (empty or fully filtered workspace)");
|
|
159
|
-
process.exit(1);
|
|
160
|
-
}
|
|
161
|
-
console.log(
|
|
162
|
-
`envelope: ${partCount} part(s), ${plainBytes} bytes plain, ${compressedBytes} bytes zstd ` +
|
|
163
|
-
`(target <= ${MAX_PART_COMPRESSED_BYTES}, rail ${MAX_PART_COMPRESSED_HARD})`,
|
|
164
|
-
);
|
|
165
|
-
if (dryRun) {
|
|
166
|
-
console.log("dry run — no upload performed");
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
if (!uploaded) {
|
|
51
|
+
const version = (process.env.OMNIRUSH_VERSION || "").trim();
|
|
52
|
+
let uploads = 0;
|
|
53
|
+
let current = accessToken;
|
|
54
|
+
const collector = new WorkspaceCollector({
|
|
55
|
+
gatewayUrl: process.env.OMNIRUSH_GATEWAY_URL || gatewayUrlForOrigin(resolveOrigin(process.env)),
|
|
56
|
+
accessToken,
|
|
57
|
+
// A dry run keeps no state (nothing is spooled for a later run).
|
|
58
|
+
...(dryRun ? {} : { stateDir: omniDir() }),
|
|
59
|
+
refreshAccessToken: async () => {
|
|
60
|
+
const refresher = sharedRefresher();
|
|
61
|
+
const refreshed = await refresher.refresh(current).catch(() => false);
|
|
62
|
+
const next = refresher.auth?.accessToken ?? null;
|
|
63
|
+
if (!refreshed || !next) return null;
|
|
64
|
+
current = next;
|
|
65
|
+
return next;
|
|
66
|
+
},
|
|
67
|
+
appVersion: version ? `omnirush-cli/${version}` : "omnirush-cli",
|
|
68
|
+
envelopeMetadata: {
|
|
69
|
+
...(version ? { client_version: version } : {}),
|
|
70
|
+
client_platform: `${process.platform}/${process.arch}`,
|
|
71
|
+
},
|
|
72
|
+
watch: false,
|
|
73
|
+
...(dryRun
|
|
74
|
+
? {
|
|
75
|
+
upload: async (_session: string, compressed: Uint8Array) => {
|
|
76
|
+
const envelope = JSON.parse(zstdDecompressSync(compressed).toString("utf8"));
|
|
77
|
+
uploads += 1;
|
|
78
|
+
console.log(
|
|
79
|
+
`${envelope.snapshot_type} snapshot: ${envelope.files.length} files, ${envelope.manifest.length} manifest entries, ` +
|
|
80
|
+
`${compressed.byteLength} bytes zstd (discarded — dry run)`,
|
|
81
|
+
);
|
|
82
|
+
return new Response(null, { status: 200 });
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
: {}),
|
|
86
|
+
log: (level, message, attributes) => {
|
|
87
|
+
// A dry run reports its envelopes itself (the upload hook above).
|
|
88
|
+
const uploaded = message === "OmniRush collection artifact uploaded" && !dryRun;
|
|
89
|
+
if (uploaded) uploads += 1;
|
|
90
|
+
if (level === "warn" || uploaded) {
|
|
91
|
+
console.error(`omnirush collect: ${message}${attributes ? ` ${JSON.stringify(attributes)}` : ""}`);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
console.log(`collect: ${dir} as session ${sessionId}`);
|
|
96
|
+
collector.startSession(sessionId, workspaceId, dir);
|
|
97
|
+
collector.finishSession(sessionId);
|
|
98
|
+
await collector.stop();
|
|
99
|
+
if (uploads === 0) {
|
|
170
100
|
console.error("omnirush collect: nothing uploaded");
|
|
171
101
|
process.exit(1);
|
|
172
102
|
}
|
|
103
|
+
console.log(dryRun ? "dry run — no upload performed" : `uploaded ${uploads} envelope(s)`);
|
|
173
104
|
}
|
|
174
105
|
|
|
175
106
|
main().catch((error) => {
|
|
@@ -6,14 +6,17 @@
|
|
|
6
6
|
// running pi entry — same runtime, same global extension/config dir)
|
|
7
7
|
// in the parent's workspace, with `--mode json -p` so their output is
|
|
8
8
|
// a parseable event stream. Each child writes its own session JSONL
|
|
9
|
-
// under <agentDir>/sessions/<cwd-slug
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// under <agentDir>/sessions/<cwd-slug>/, under a session id the parent
|
|
10
|
+
// picks (`--session-id`) and returns in the result (`session_id`): the
|
|
11
|
+
// parent session's collector reads the child's session file from it and
|
|
12
|
+
// records the child as a sub-agent of the turn (session.child events,
|
|
13
|
+
// grandchildren included), the way the desktop records task sub-agents.
|
|
12
14
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
+
// Children get OMNIRUSH_PARENT_SESSION=<parent session id> in their
|
|
16
|
+
// environment: a child process uploads nothing itself.
|
|
15
17
|
|
|
16
18
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
17
20
|
import { existsSync } from "node:fs";
|
|
18
21
|
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
19
22
|
import { tmpdir } from "node:os";
|
|
@@ -23,12 +26,6 @@ import path from "node:path";
|
|
|
23
26
|
export const CHILD_TIMEOUT_MS = 10 * 60_000;
|
|
24
27
|
/** Grace between SIGTERM and SIGKILL on timeout. */
|
|
25
28
|
export const CHILD_KILL_GRACE_MS = 5_000;
|
|
26
|
-
/** Default number of children running at once. */
|
|
27
|
-
export const MAX_CONCURRENT_CHILDREN = 4;
|
|
28
|
-
/** Upper bound on children running at once (swarms). */
|
|
29
|
-
export const MAX_CONCURRENCY_CEILING = 12;
|
|
30
|
-
/** Upper bound on tasks per spawn_agents call. */
|
|
31
|
-
export const MAX_TASKS_PER_CALL = 12;
|
|
32
29
|
/** Per-child output cap in the structured result (bytes, UTF-8). */
|
|
33
30
|
export const CHILD_OUTPUT_CAP_BYTES = 50 * 1024;
|
|
34
31
|
|
|
@@ -114,8 +111,14 @@ export function buildChildArgs(
|
|
|
114
111
|
task: string,
|
|
115
112
|
promptFilePath: string | null,
|
|
116
113
|
model?: string,
|
|
114
|
+
sessionId?: string,
|
|
117
115
|
): string[] {
|
|
118
116
|
const args: string[] = ["--mode", "json", "-p"];
|
|
117
|
+
if (sessionId) {
|
|
118
|
+
// The child's session id, picked by the parent so its session file can
|
|
119
|
+
// be found and captured as this session's sub-agent.
|
|
120
|
+
args.push("--session-id", sessionId);
|
|
121
|
+
}
|
|
119
122
|
if (promptFilePath) {
|
|
120
123
|
args.push("--append-system-prompt", promptFilePath);
|
|
121
124
|
}
|
|
@@ -159,6 +162,8 @@ export type ChildStatus = "completed" | "failed" | "timeout" | "cancelled";
|
|
|
159
162
|
export interface ChildResult {
|
|
160
163
|
role: AgentRole;
|
|
161
164
|
task: string;
|
|
165
|
+
/** The child's pi session id (its session file is `<ts>_<id>.jsonl`). */
|
|
166
|
+
session_id?: string;
|
|
162
167
|
/** The gateway model the child ran on (cross-model children). */
|
|
163
168
|
model?: string;
|
|
164
169
|
status: ChildStatus;
|
|
@@ -187,6 +192,7 @@ export function buildChildResult(
|
|
|
187
192
|
task: ChildTask,
|
|
188
193
|
input: ChildResultInput,
|
|
189
194
|
durationMs: number,
|
|
195
|
+
sessionId?: string,
|
|
190
196
|
): ChildResult {
|
|
191
197
|
const output = input.finalText;
|
|
192
198
|
let capped = output;
|
|
@@ -198,6 +204,7 @@ export function buildChildResult(
|
|
|
198
204
|
return {
|
|
199
205
|
role: task.role,
|
|
200
206
|
task: task.task,
|
|
207
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
201
208
|
...(task.model ? { model: task.model } : {}),
|
|
202
209
|
status: input.status ?? (input.exitCode === 0 ? "completed" : "failed"),
|
|
203
210
|
exitCode: input.exitCode,
|
|
@@ -266,6 +273,8 @@ export interface SpawnChildOptions {
|
|
|
266
273
|
now?: () => number;
|
|
267
274
|
/** Progress callback: partial output lines as the child runs. */
|
|
268
275
|
onChildStdout?: (role: AgentRole, chunk: string) => void;
|
|
276
|
+
/** The child's session id (a fresh UUID by default). */
|
|
277
|
+
sessionId?: string;
|
|
269
278
|
}
|
|
270
279
|
|
|
271
280
|
/**
|
|
@@ -282,9 +291,10 @@ export async function runChildAgent(
|
|
|
282
291
|
const startedAt = (options.now ?? Date.now)();
|
|
283
292
|
const timeoutMs = Math.max(1_000, options.timeoutMs ?? CHILD_TIMEOUT_MS);
|
|
284
293
|
const spawnImpl = options.spawnImpl ?? nodeSpawn;
|
|
294
|
+
const sessionId = options.sessionId ?? randomUUID();
|
|
285
295
|
|
|
286
296
|
return withRolePromptFile(task.role, async (promptFile) => {
|
|
287
|
-
const args = buildChildArgs(task.task, promptFile, task.model);
|
|
297
|
+
const args = buildChildArgs(task.task, promptFile, task.model, sessionId);
|
|
288
298
|
const invocation = childInvocation(args);
|
|
289
299
|
|
|
290
300
|
return await new Promise<ChildResult>((resolvePromise) => {
|
|
@@ -311,7 +321,7 @@ export async function runChildAgent(
|
|
|
311
321
|
options.signal.removeEventListener("abort", abortHandler);
|
|
312
322
|
}
|
|
313
323
|
resolvePromise(
|
|
314
|
-
buildChildResult(task, input, (options.now ?? Date.now)() - startedAt),
|
|
324
|
+
buildChildResult(task, input, (options.now ?? Date.now)() - startedAt, sessionId),
|
|
315
325
|
);
|
|
316
326
|
};
|
|
317
327
|
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
// - structured results returned to the parent model
|
|
11
11
|
//
|
|
12
12
|
// Tracing is free: every child writes its own session JSONL under
|
|
13
|
-
// <agentDir>/sessions/<cwd-slug>/ (children run without --no-session)
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
13
|
+
// <agentDir>/sessions/<cwd-slug>/ (children run without --no-session)
|
|
14
|
+
// under a session id the parent picks, and the parent session's
|
|
15
|
+
// collector records each child's conversation as a sub-agent of the
|
|
16
|
+
// turn (session.child events with parent and depth, as the desktop
|
|
17
|
+
// records task sub-agents). Children get OMNIRUSH_PARENT_SESSION, so a
|
|
18
|
+
// child process uploads nothing of its own.
|
|
19
19
|
//
|
|
20
20
|
// `/agents` lists this session's subagents (running + finished).
|
|
21
21
|
|
|
@@ -27,9 +27,6 @@ import {
|
|
|
27
27
|
AGENT_ROLES,
|
|
28
28
|
CHILD_TIMEOUT_MS,
|
|
29
29
|
mapWithConcurrency,
|
|
30
|
-
MAX_CONCURRENT_CHILDREN,
|
|
31
|
-
MAX_CONCURRENCY_CEILING,
|
|
32
|
-
MAX_TASKS_PER_CALL,
|
|
33
30
|
renderChildResults,
|
|
34
31
|
runChildAgent,
|
|
35
32
|
type ChildResult,
|
|
@@ -47,7 +44,7 @@ const SpawnAgentsParams = Type.Object({
|
|
|
47
44
|
description: 'Cross-model child: gateway model id for THIS task (e.g. "muse-spark-1.3", "muse-spark-1.2-contributor" for cheap swarm workers, "gpt-6-astra", "gpt-5.6-sol"). Omit to inherit this session\'s model',
|
|
48
45
|
})),
|
|
49
46
|
}),
|
|
50
|
-
{ description: "Tasks to delegate; they run in parallel", minItems: 1
|
|
47
|
+
{ description: "Tasks to delegate; they all run in parallel", minItems: 1 },
|
|
51
48
|
),
|
|
52
49
|
timeout_minutes: Type.Optional(Type.Number({
|
|
53
50
|
description: `Per-child wall-clock limit in minutes (default ${CHILD_TIMEOUT_MS / 60_000}; a timed-out child is killed and its partial result is kept)`,
|
|
@@ -55,9 +52,8 @@ const SpawnAgentsParams = Type.Object({
|
|
|
55
52
|
maximum: 60,
|
|
56
53
|
})),
|
|
57
54
|
max_parallel: Type.Optional(Type.Number({
|
|
58
|
-
description:
|
|
55
|
+
description: "Optional throttle: run at most this many children at once. Omit to run every task in parallel (no cap)",
|
|
59
56
|
minimum: 1,
|
|
60
|
-
maximum: MAX_CONCURRENCY_CEILING,
|
|
61
57
|
})),
|
|
62
58
|
});
|
|
63
59
|
|
|
@@ -80,7 +76,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
80
76
|
"Delegate tasks to parallel subagents with isolated context windows.",
|
|
81
77
|
"Each child is a headless agent run in this workspace with a role preset:",
|
|
82
78
|
"code-searcher (read-only exploration), researcher-web (web research via web_search + web_fetch), general-worker (full tools).",
|
|
83
|
-
`Children run in parallel
|
|
79
|
+
`Children run in parallel — every task at once unless max_parallel throttles it — and each gets its own timeout (default ${CHILD_TIMEOUT_MS / 60_000} min).`,
|
|
84
80
|
'Each task can run on a DIFFERENT model via its "model" field — e.g. cheap muse workers for wide swarm sweeps under an astra parent.',
|
|
85
81
|
"Every task description must be SELF-CONTAINED: the child cannot see this conversation.",
|
|
86
82
|
"Use for parallelizable work: broad code surveys, independent research questions, independent implementation chunks.",
|
|
@@ -109,21 +105,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
109
105
|
});
|
|
110
106
|
}
|
|
111
107
|
if (tasks.length === 0) throw new Error("no tasks given");
|
|
112
|
-
if (tasks.length > MAX_TASKS_PER_CALL) {
|
|
113
|
-
throw new Error(`too many tasks (${tasks.length}); the limit is ${MAX_TASKS_PER_CALL} per call`);
|
|
114
|
-
}
|
|
115
108
|
const parentSessionId = String(ctx?.sessionManager?.getSessionId?.() ?? "");
|
|
116
109
|
if (!parentSessionId) throw new Error("no parent session id — subagents cannot be traced");
|
|
117
110
|
const cwd = ctx?.cwd ? String(ctx.cwd) : process.cwd();
|
|
118
111
|
const timeoutMinutes = typeof params.timeout_minutes === "number" && params.timeout_minutes > 0
|
|
119
112
|
? params.timeout_minutes
|
|
120
113
|
: undefined;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Math.
|
|
124
|
-
|
|
125
|
-
: MAX_CONCURRENT_CHILDREN),
|
|
126
|
-
);
|
|
114
|
+
// No cap: every task runs at once unless the caller throttles.
|
|
115
|
+
const concurrency = typeof params.max_parallel === "number" && params.max_parallel >= 1
|
|
116
|
+
? Math.floor(params.max_parallel)
|
|
117
|
+
: tasks.length;
|
|
127
118
|
|
|
128
119
|
const entries = registry.get(parentSessionId) ?? [];
|
|
129
120
|
registry.set(parentSessionId, entries);
|
|
@@ -139,6 +130,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
139
130
|
return entry;
|
|
140
131
|
});
|
|
141
132
|
|
|
133
|
+
// Children settled so far, for progress updates (the result array
|
|
134
|
+
// below only exists once every child settled).
|
|
135
|
+
const settled: ChildResult[] = [];
|
|
142
136
|
const results = await mapWithConcurrency<ChildTask, ChildResult>(
|
|
143
137
|
tasks,
|
|
144
138
|
concurrency,
|
|
@@ -152,9 +146,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
152
146
|
const mark = marks[index];
|
|
153
147
|
mark.status = result.status;
|
|
154
148
|
mark.durationMs = result.durationMs;
|
|
149
|
+
settled.push(result);
|
|
155
150
|
onUpdate?.({
|
|
156
|
-
content: [{ type: "text", text: `spawn_agents: ${
|
|
157
|
-
details: { results: [...
|
|
151
|
+
content: [{ type: "text", text: `spawn_agents: ${settled.length}/${tasks.length} settled (latest: ${task.role})` }],
|
|
152
|
+
details: { results: [...settled] },
|
|
158
153
|
});
|
|
159
154
|
return result;
|
|
160
155
|
},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Ported from omnirush-ai/omnirush-gui @ 2e166a9f9843dccbedc8277addcd94b752e66adb
|
|
2
|
+
(apps/server/src), unchanged except for the CLI seams marked "CLI:" in the code:
|
|
3
|
+
|
|
4
|
+
workspace-collector.ts vendored minimatch import; exported ledger reader and
|
|
5
|
+
.gitignore helpers; `watch: false` (poll on snapshots)
|
|
6
|
+
and `envelopeMetadata` options
|
|
7
|
+
turn-diff.ts unchanged
|
|
8
|
+
collect-upload-budget.ts unchanged
|
|
9
|
+
session-archive/detect.ts, files.ts, lifecycle.ts, pack.ts, seal.ts unchanged
|
|
10
|
+
session-archive/index.ts, policy.ts, upload.ts vendored zod import
|
|
11
|
+
session-archive/manifest.ts, touched.ts gitignored content left out
|
|
12
|
+
(`ignored` counter, runGit exported)
|
|
13
|
+
|
|
14
|
+
CLI files beside them:
|
|
15
|
+
session-archive/ignored.ts git's ignore rules for the archive scans
|
|
16
|
+
server-fetch.ts external egress = the global fetch
|
|
17
|
+
omnirush-swarm.ts the one constant the collector reads
|
|
18
|
+
vendor/ zod and minimatch (npm run build:capture-vendor)
|
|
19
|
+
|
|
20
|
+
Resync: copy the files again, re-apply the seams, then run
|
|
21
|
+
dev/gui-parity-tests.sh <omnirush-gui checkout>.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long one collector upload (POST <gateway>/collect) may take.
|
|
3
|
+
*
|
|
4
|
+
* Envelopes range from a few KiB (a change snapshot) to tens of MiB (the
|
|
5
|
+
* start snapshot of a large workspace), so the deadline grows with the body:
|
|
6
|
+
*
|
|
7
|
+
* - `baseMs` covers connecting, TLS and the first bytes;
|
|
8
|
+
* - the body is allowed `bytesPerSecond`, a conservative uplink rate, capped
|
|
9
|
+
* so that this sending part never exceeds `maxSendMs`;
|
|
10
|
+
* - `responseMs` is the wait for the answer once the body is sent: the
|
|
11
|
+
* gateway scrubs a large envelope before it answers.
|
|
12
|
+
*
|
|
13
|
+
* The fetch implementations the uploads go through (undici in Node and
|
|
14
|
+
* Electron, Electron's net.fetch, Bun's fetch) report no upload progress, so
|
|
15
|
+
* the moment the last body byte left is not observable and the response wait
|
|
16
|
+
* cannot run as a timer of its own. The parts therefore form one deadline for
|
|
17
|
+
* the whole request, computed from the envelope's size before it is sent: a
|
|
18
|
+
* body sent faster than the assumed rate leaves the unused sending time to the
|
|
19
|
+
* response wait.
|
|
20
|
+
*/
|
|
21
|
+
export type CollectUploadBudget = {
|
|
22
|
+
baseMs: number;
|
|
23
|
+
bytesPerSecond: number;
|
|
24
|
+
maxSendMs: number;
|
|
25
|
+
responseMs: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const COLLECT_UPLOAD_BUDGET: CollectUploadBudget = {
|
|
29
|
+
baseMs: 30_000,
|
|
30
|
+
bytesPerSecond: 128 * 1024,
|
|
31
|
+
maxSendMs: 15 * 60_000,
|
|
32
|
+
responseMs: 120_000,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** The whole-request deadline, in milliseconds, for a collect upload of `bytes`. */
|
|
36
|
+
export function collectUploadTimeoutMs(bytes: number, budget: CollectUploadBudget = COLLECT_UPLOAD_BUDGET): number {
|
|
37
|
+
const sendMs = budget.baseMs + Math.ceil((Math.max(0, bytes) * 1_000) / budget.bytesPerSecond);
|
|
38
|
+
return Math.min(budget.maxSendMs, sendMs) + budget.responseMs;
|
|
39
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The one constant the ported workspace collector reads from the desktop's
|
|
2
|
+
// swarm module (apps/server/src/omnirush-swarm.ts): the trace event that
|
|
3
|
+
// records a sub-agent answered on the main model instead of the picked one.
|
|
4
|
+
// The CLI never emits it (its sub-agents run in their own process on the
|
|
5
|
+
// model they were given), but the collector keeps the same handling.
|
|
6
|
+
export const SUBAGENT_MODEL_FALLBACK_TRACE = "subagent.model_fallback";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Egress seam of the ported capture modules (desktop: apps/server/src/
|
|
2
|
+
// server-fetch.ts). The desktop routes external egress through the embedding
|
|
3
|
+
// runtime's network stack; the CLI has one runtime and one fetch, so every
|
|
4
|
+
// upload (collect envelopes, archive API calls, presigned S3 part PUTs) goes
|
|
5
|
+
// through the global fetch.
|
|
6
|
+
export function externalFetch(input: string, init?: RequestInit): Promise<Response> {
|
|
7
|
+
return globalThis.fetch(input, init);
|
|
8
|
+
}
|