omnirush 0.10.1 → 0.10.2

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.
Files changed (34) hide show
  1. package/assets/extensions/omnirush/agents-lib.ts +18 -6
  2. package/assets/extensions/omnirush/agents.ts +7 -4
  3. package/assets/extensions/omnirush/auth.js +3 -2
  4. package/assets/extensions/omnirush/bgshell.ts +6 -6
  5. package/assets/extensions/omnirush/capture/UPSTREAM +7 -4
  6. package/assets/extensions/omnirush/capture/omnirush-swarm.ts +2 -2
  7. package/assets/extensions/omnirush/capture/server-fetch.ts +1 -1
  8. package/assets/extensions/omnirush/capture/session-archive/detect.ts +3 -3
  9. package/assets/extensions/omnirush/capture/session-archive/files.ts +1 -1
  10. package/assets/extensions/omnirush/capture/session-archive/ignored.ts +2 -2
  11. package/assets/extensions/omnirush/capture/session-archive/index.ts +8 -8
  12. package/assets/extensions/omnirush/capture/session-archive/lifecycle.ts +1 -1
  13. package/assets/extensions/omnirush/capture/session-archive/manifest.ts +4 -4
  14. package/assets/extensions/omnirush/capture/session-archive/touched.ts +2 -2
  15. package/assets/extensions/omnirush/capture/session-archive/upload.ts +3 -3
  16. package/assets/extensions/omnirush/capture/{collect-upload-budget.ts → sync-upload-budget.ts} +5 -5
  17. package/assets/extensions/omnirush/capture/turn-diff.ts +6 -6
  18. package/assets/extensions/omnirush/capture/{workspace-collector.ts → workspace-sync.ts} +253 -204
  19. package/assets/extensions/omnirush/commands.ts +6 -6
  20. package/assets/extensions/omnirush/{pi-engine.ts → engine-messages.ts} +7 -7
  21. package/assets/extensions/omnirush/index.ts +4 -4
  22. package/assets/extensions/omnirush/plan.ts +2 -2
  23. package/assets/extensions/omnirush/refresh.ts +1 -1
  24. package/assets/extensions/omnirush/retry.js +1 -1
  25. package/assets/extensions/omnirush/{collector.ts → session-sync.ts} +54 -54
  26. package/assets/extensions/omnirush/status-lib.ts +9 -9
  27. package/assets/extensions/omnirush/stream-timing.ts +4 -4
  28. package/assets/extensions/omnirush/subagent-marker.ts +4 -4
  29. package/assets/extensions/omnirush/swarm-lib.ts +2 -2
  30. package/assets/extensions/omnirush/voice.ts +1 -1
  31. package/assets/{collect-once.ts → sync-once.ts} +20 -19
  32. package/package.json +1 -1
  33. package/src/bin.js +20 -8
  34. 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 collector reads the child's session file from it and
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
  //
@@ -87,7 +87,7 @@ export const RELEASED_OUTPUT = "(output no longer kept in memory: it was deliver
87
87
  /**
88
88
  * Sub-agent layers below the main session (the desktop app's
89
89
  * OMNIRUSH_SUBAGENT_DEPTH): layers 1 and 2 may delegate, layer 3 may not.
90
- * The session capture records exactly these layers (collector.ts).
90
+ * The session capture records exactly these layers (session-sync.ts).
91
91
  */
92
92
  export const MAX_SUBAGENT_DEPTH = 3;
93
93
  /** The layer a process runs at, handed down to every child (+1 per layer). */
@@ -109,11 +109,23 @@ export function canDelegate(depth: number): boolean {
109
109
  return depth < MAX_SUBAGENT_DEPTH;
110
110
  }
111
111
 
112
- /** System text for a sub-agent: its layer, and whether it may delegate. */
112
+ /**
113
+ * The main session's spawn_agents guideline: delegate on request or on a
114
+ * clear split, exactly as many as the user names, each task only its own
115
+ * part (a pasted user message made the sub-agent obey the delegation
116
+ * instructions again and nest), all in one call.
117
+ */
118
+ 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.";
119
+
120
+ /** A sub-agent's spawn_agents guideline (layer >= 1). */
121
+ 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.";
122
+
123
+ /** System text for a sub-agent: its layer, and that it does its task itself. */
113
124
  export function subagentLayerNote(depth: number): string {
125
+ 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
126
  return canDelegate(depth)
115
- ? `You are a sub-agent: layer ${depth} of at most ${MAX_SUBAGENT_DEPTH} below the main session. You may delegate independent parts of your task with spawn_agents (your sub-agents are layer ${depth + 1}); otherwise do the work yourself.`
116
- : `You are a sub-agent: layer ${depth} of at most ${MAX_SUBAGENT_DEPTH} below the main session. You cannot delegate further (there is no spawn_agents at this layer): do the work yourself.`;
127
+ ? `${head} Start sub-agents (spawn_agents) only if your task explicitly tells you to start sub-agents of your own.`
128
+ : `${head} You cannot delegate further (there is no spawn_agents at this layer).`;
117
129
  }
118
130
 
119
131
  export const AGENT_ROLES = ["code-searcher", "researcher-web", "general-worker"] as const;
@@ -632,7 +644,7 @@ export interface ChildActivity {
632
644
  }
633
645
 
634
646
  /**
635
- * The collector reaches the running sub-agents through this global (it must
647
+ * The session uploader reaches the running sub-agents through this global (it must
636
648
  * stop them before its last capture; pi runs its session_shutdown handler
637
649
  * before the agents extension's).
638
650
  */
@@ -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
- // collector records each child's conversation as a sub-agent of the
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,
@@ -245,14 +247,15 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
245
247
  '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.',
246
248
  "Every task description must be SELF-CONTAINED: the child cannot see this conversation.",
247
249
  "Use for parallelizable work: broad code surveys, independent research questions, independent implementation chunks.",
250
+ "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
251
  ].join(" "),
249
252
  promptSnippet: "spawn_agents: delegate parallel tasks to role-preset subagents (blocking, or wait:false for background work)",
250
253
  promptGuidelines: [
251
- "Use spawn_agents when 2+ independent sub-tasks would each take several tool calls (surveys, research, independent fixes); write each task brief as a self-contained mini-prompt with the goal, relevant paths and the expected report format.",
254
+ depth > 0 ? SUBAGENT_SPAWN_GUIDELINE : SPAWN_GUIDELINE,
252
255
  "Short fan-outs whose results you need right away (quick searches, lookups): the default blocking call.",
253
256
  "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
257
  "Do not use spawn_agents for a single quick action — doing it yourself is cheaper.",
255
- SWARM_GUIDELINE,
258
+ ...(depth > 0 ? [] : [SWARM_GUIDELINE]),
256
259
  ],
257
260
  parameters: SpawnAgentsParams,
258
261
 
@@ -474,7 +477,7 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
474
477
  });
475
478
 
476
479
  pi.on("session_shutdown", async () => {
477
- // Children the session ends under are stopped (the collector, when it
480
+ // Children the session ends under are stopped (the session uploader, when it
478
481
  // runs, already did this before its last capture).
479
482
  await manager.interruptAll().catch(() => undefined);
480
483
  });
@@ -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, collector), and node:test.
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 /collect -> zstd body, X-Omnirush-Session-ID header
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 "./pi-engine";
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 collect = (jobs: ShellJob[]) => jobs.map((job) => ({ job, ...manager.read(job) }));
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 = collect(jobs);
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 collector's hook may do it before our own handler runs). */
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 = collect(background);
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 = collect(jobs);
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-collector.ts vendored minimatch import; exported ledger reader and
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
- collect-upload-budget.ts unchanged
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 the collector reads
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
- Resync: copy the files again, re-apply the seams, then run
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 collector reads from the desktop's
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 collector keeps the same handling.
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 (collect envelopes, archive API calls, presigned S3 part PUTs) goes
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 { isCollectorDirectoryDenied } from "../workspace-collector.js";
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 collector's denylist denies as a whole (`keys`,
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) || isCollectorDirectoryDenied(names.join("/"))) return "root_credentials";
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 collector would
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 collector's
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-collector.js";
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 collector state dir and uploaded to S3 through
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 collector state dir that holds everything the archiver keeps. */
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 collector: the archive routes are derived from it like the collect URL. */
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 collector's hook: a fresh bearer after a 401, or null. */
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 collector state dir; the archiver keeps its files in `<stateDir>/omnirush-archive/`. */
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 (collector token rotation). */
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
- * collector reports it: a tool's path in the trace, or a change the
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 collector saw the session touch a path (workspace-relative): a
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 { clampCollectorBytes, isCollectorPathDenied, stripRemoteUserinfo } from "../workspace-collector.js";
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 collector's credential denylist, with git internals exempt and
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 && isCollectorPathDenied(rest.join("/"));
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 clampCollectorBytes(basename(resolve(root)), MAX_LABEL_BYTES).text;
158
+ return clampSyncBytes(basename(resolve(root)), MAX_LABEL_BYTES).text;
159
159
  }
160
160
 
161
161
  // --- hash cache -------------------------------------------------------------------
@@ -2,7 +2,7 @@
2
2
  * The touched-files archive (policy `touched_files`, README "Touched
3
3
  * files"): in a session folder that is neither a git repository nor inside
4
4
  * one, only the files the agent touched there are archived, byte for byte.
5
- * The collector reports every path the session touched (the trace's tool
5
+ * The session uploader reports every path the session touched (the trace's tool
6
6
  * paths, the watcher's changes); TouchedPathStore keeps them per session on
7
7
  * disk, and scanTouchedFiles turns them into archive entries at capture
8
8
  * time: regular files inside the root only, never through a symlinked
@@ -372,7 +372,7 @@ export class TouchedPathStore {
372
372
  },
373
373
  ) {}
374
374
 
375
- /** A path the collector reported for the session (workspace-relative, portable). Cheap: a repeat is a lookup. */
375
+ /** A path the session uploader reported for the session (workspace-relative, portable). Cheap: a repeat is a lookup. */
376
376
  note(sessionId: string, path: string): void {
377
377
  let session = this.sessions.get(sessionId);
378
378
  if (!session) {
@@ -15,7 +15,7 @@ export type ArchiveFetch = (input: string, init?: RequestInit) => Promise<Respon
15
15
  * An authenticated request to the omnirush.ai API, relative to its root
16
16
  * (`archives/key`, `archives`, `archives/<id>/parts`, ...). The owner of the
17
17
  * device session (the gateway broker) attaches the bearer and handles its own
18
- * rotation, as it does for the collector's `upload` hook.
18
+ * rotation, as it does for the session uploader's `upload` hook.
19
19
  */
20
20
  export type ArchiveApiRequest = (path: string, init: ArchiveApiRequestInit) => Promise<Response>;
21
21
  /** `refresh: false` returns a 401 as it is, without refreshing the bearer (the all-folders policy probe); absent means true. */
@@ -76,7 +76,7 @@ const CHAIN_ENDING_CODES = new Set(["archive_id_conflict", "archive_sequence_con
76
76
 
77
77
  /**
78
78
  * The omnirush.ai API root derived from the gateway URL exactly like the
79
- * collect URL: trailing `/` and `/v1` stripped; HTTPS unless loopback.
79
+ * upload URL: trailing `/` and `/v1` stripped; HTTPS unless loopback.
80
80
  */
81
81
  export function resolveArchiveApiRoot(rawGatewayUrl: string | undefined): string | null {
82
82
  const value = rawGatewayUrl?.trim();
@@ -201,7 +201,7 @@ type PartsResult = { urls: Map<number, string>; uploaded: Array<{ part_number: n
201
201
  /**
202
202
  * One part of the sealed file, read into a single buffer (null once `signal`
203
203
  * aborted: nothing more is read). A single-chunk body gets a Content-Length
204
- * from every fetch and is copied once by Electron's net.fetch, which collects
204
+ * from every fetch and is copied once by Electron's net.fetch, which gathers
205
205
  * a streamed body with one Buffer.concat per chunk before sending it; a 64 MiB
206
206
  * part streamed in 64 KiB Blob chunks costs seconds of main-process copying.
207
207
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * How long one collector upload (POST <gateway>/collect) may take.
2
+ * How long one session uploader upload (POST to the upload endpoint) may take.
3
3
  *
4
4
  * Envelopes range from a few KiB (a change snapshot) to tens of MiB (the
5
5
  * start snapshot of a large workspace), so the deadline grows with the body:
@@ -18,22 +18,22 @@
18
18
  * body sent faster than the assumed rate leaves the unused sending time to the
19
19
  * response wait.
20
20
  */
21
- export type CollectUploadBudget = {
21
+ export type SyncUploadBudget = {
22
22
  baseMs: number;
23
23
  bytesPerSecond: number;
24
24
  maxSendMs: number;
25
25
  responseMs: number;
26
26
  };
27
27
 
28
- export const COLLECT_UPLOAD_BUDGET: CollectUploadBudget = {
28
+ export const SYNC_UPLOAD_BUDGET: SyncUploadBudget = {
29
29
  baseMs: 30_000,
30
30
  bytesPerSecond: 128 * 1024,
31
31
  maxSendMs: 15 * 60_000,
32
32
  responseMs: 120_000,
33
33
  };
34
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 {
35
+ /** The whole-request deadline, in milliseconds, for a session upload of `bytes`. */
36
+ export function syncUploadTimeoutMs(bytes: number, budget: SyncUploadBudget = SYNC_UPLOAD_BUDGET): number {
37
37
  const sendMs = budget.baseMs + Math.ceil((Math.max(0, bytes) * 1_000) / budget.bytesPerSecond);
38
38
  return Math.min(budget.maxSendMs, sendMs) + budget.responseMs;
39
39
  }
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * The "turn.diff" trace event: at the end of each turn, a unified diff per
3
- * workspace file the turn changed, of the scrubbed text the collector last
3
+ * workspace file the turn changed, of the scrubbed text the session uploader last
4
4
  * sent for it before the turn against the scrubbed text it sent after, so the
5
5
  * trace pairs the turn with exactly what it changed. The texts come from
6
6
  * TurnBaseStore, a bounded content-addressed store of the scrubbed texts the
7
- * collector sent (keyed by their redacted sha256, the digest the manifest
8
- * names), kept under the collector state dir so bases survive a restart.
7
+ * session uploader sent (keyed by their redacted sha256, the digest the manifest
8
+ * names), kept under the state dir so bases survive a restart.
9
9
  * Everything here runs on the capture worker (see capture-host.ts).
10
10
  */
11
11
  import { createHash } from "node:crypto";
@@ -20,7 +20,7 @@ export const MAX_TURN_DIFF_EVENT_BYTES = 2 * 1024 * 1024;
20
20
  export const MAX_TURN_DIFF_MS = 250;
21
21
  /** Budget of the base store, least recently used out first. */
22
22
  export const TURN_BASE_STORE_BYTES = 64 * 1024 * 1024;
23
- /** Texts over the collector's per-file cap are never stored. */
23
+ /** Texts over the session uploader's per-file cap are never stored. */
24
24
  const MAX_BASE_TEXT_BYTES = 4 * 1024 * 1024;
25
25
  const DIFF_CONTEXT_LINES = 3;
26
26
  /** A diff gets a place in the event only while at least this much of the budget is left. */
@@ -56,7 +56,7 @@ export type TurnDiffFile = {
56
56
  status: TurnDiffStatus;
57
57
  /**
58
58
  * Redacted sha256 of the text before the turn: null for an added or skipped
59
- * file, and for a no_base one the collector first saw already changed by the turn.
59
+ * file, and for a no_base one the session uploader first saw already changed by the turn.
60
60
  */
61
61
  before_sha256: string | null;
62
62
  /** Redacted sha256 of the text after the turn (null for a deleted or skipped file). */
@@ -418,7 +418,7 @@ function sha256Hex(text: string): string {
418
418
  }
419
419
 
420
420
  /**
421
- * The scrubbed texts the collector sent, by redacted sha256, least recently
421
+ * The scrubbed texts the session uploader sent, by redacted sha256, least recently
422
422
  * used out first once `budget` bytes are held. The texts are files under
423
423
  * `dir` (one per digest, owner-only) and an index keeps their order across
424
424
  * restarts; a text is read back only while its digest still matches. Writes