omnirush 0.7.1 → 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 -7
- package/assets/extensions/omnirush/agents.ts +12 -8
- 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";
|
|
@@ -108,8 +111,14 @@ export function buildChildArgs(
|
|
|
108
111
|
task: string,
|
|
109
112
|
promptFilePath: string | null,
|
|
110
113
|
model?: string,
|
|
114
|
+
sessionId?: string,
|
|
111
115
|
): string[] {
|
|
112
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
|
+
}
|
|
113
122
|
if (promptFilePath) {
|
|
114
123
|
args.push("--append-system-prompt", promptFilePath);
|
|
115
124
|
}
|
|
@@ -153,6 +162,8 @@ export type ChildStatus = "completed" | "failed" | "timeout" | "cancelled";
|
|
|
153
162
|
export interface ChildResult {
|
|
154
163
|
role: AgentRole;
|
|
155
164
|
task: string;
|
|
165
|
+
/** The child's pi session id (its session file is `<ts>_<id>.jsonl`). */
|
|
166
|
+
session_id?: string;
|
|
156
167
|
/** The gateway model the child ran on (cross-model children). */
|
|
157
168
|
model?: string;
|
|
158
169
|
status: ChildStatus;
|
|
@@ -181,6 +192,7 @@ export function buildChildResult(
|
|
|
181
192
|
task: ChildTask,
|
|
182
193
|
input: ChildResultInput,
|
|
183
194
|
durationMs: number,
|
|
195
|
+
sessionId?: string,
|
|
184
196
|
): ChildResult {
|
|
185
197
|
const output = input.finalText;
|
|
186
198
|
let capped = output;
|
|
@@ -192,6 +204,7 @@ export function buildChildResult(
|
|
|
192
204
|
return {
|
|
193
205
|
role: task.role,
|
|
194
206
|
task: task.task,
|
|
207
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
195
208
|
...(task.model ? { model: task.model } : {}),
|
|
196
209
|
status: input.status ?? (input.exitCode === 0 ? "completed" : "failed"),
|
|
197
210
|
exitCode: input.exitCode,
|
|
@@ -260,6 +273,8 @@ export interface SpawnChildOptions {
|
|
|
260
273
|
now?: () => number;
|
|
261
274
|
/** Progress callback: partial output lines as the child runs. */
|
|
262
275
|
onChildStdout?: (role: AgentRole, chunk: string) => void;
|
|
276
|
+
/** The child's session id (a fresh UUID by default). */
|
|
277
|
+
sessionId?: string;
|
|
263
278
|
}
|
|
264
279
|
|
|
265
280
|
/**
|
|
@@ -276,9 +291,10 @@ export async function runChildAgent(
|
|
|
276
291
|
const startedAt = (options.now ?? Date.now)();
|
|
277
292
|
const timeoutMs = Math.max(1_000, options.timeoutMs ?? CHILD_TIMEOUT_MS);
|
|
278
293
|
const spawnImpl = options.spawnImpl ?? nodeSpawn;
|
|
294
|
+
const sessionId = options.sessionId ?? randomUUID();
|
|
279
295
|
|
|
280
296
|
return withRolePromptFile(task.role, async (promptFile) => {
|
|
281
|
-
const args = buildChildArgs(task.task, promptFile, task.model);
|
|
297
|
+
const args = buildChildArgs(task.task, promptFile, task.model, sessionId);
|
|
282
298
|
const invocation = childInvocation(args);
|
|
283
299
|
|
|
284
300
|
return await new Promise<ChildResult>((resolvePromise) => {
|
|
@@ -305,7 +321,7 @@ export async function runChildAgent(
|
|
|
305
321
|
options.signal.removeEventListener("abort", abortHandler);
|
|
306
322
|
}
|
|
307
323
|
resolvePromise(
|
|
308
|
-
buildChildResult(task, input, (options.now ?? Date.now)() - startedAt),
|
|
324
|
+
buildChildResult(task, input, (options.now ?? Date.now)() - startedAt, sessionId),
|
|
309
325
|
);
|
|
310
326
|
};
|
|
311
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
|
|
|
@@ -130,6 +130,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
130
130
|
return entry;
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
// Children settled so far, for progress updates (the result array
|
|
134
|
+
// below only exists once every child settled).
|
|
135
|
+
const settled: ChildResult[] = [];
|
|
133
136
|
const results = await mapWithConcurrency<ChildTask, ChildResult>(
|
|
134
137
|
tasks,
|
|
135
138
|
concurrency,
|
|
@@ -143,9 +146,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
143
146
|
const mark = marks[index];
|
|
144
147
|
mark.status = result.status;
|
|
145
148
|
mark.durationMs = result.durationMs;
|
|
149
|
+
settled.push(result);
|
|
146
150
|
onUpdate?.({
|
|
147
|
-
content: [{ type: "text", text: `spawn_agents: ${
|
|
148
|
-
details: { results: [...
|
|
151
|
+
content: [{ type: "text", text: `spawn_agents: ${settled.length}/${tasks.length} settled (latest: ${task.role})` }],
|
|
152
|
+
details: { results: [...settled] },
|
|
149
153
|
});
|
|
150
154
|
return result;
|
|
151
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
|
+
}
|