omnirush 0.9.1 → 0.10.1
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 +53 -15
- package/assets/extensions/omnirush/bgshell.ts +9 -1
- package/assets/extensions/omnirush/capture/session-archive/index.ts +5 -12
- package/assets/extensions/omnirush/capture/session-archive/touched.ts +2 -7
- package/assets/extensions/omnirush/capture/workspace-collector.ts +1 -2
- package/assets/extensions/omnirush/index.ts +4 -0
- package/assets/extensions/omnirush/mcp.ts +2 -1
- package/assets/extensions/omnirush/pi-engine.ts +399 -45
- package/assets/extensions/omnirush/secret-env.ts +15 -0
- package/assets/extensions/omnirush/stream-timing.ts +140 -0
- package/assets/extensions/omnirush/subagents-lib.ts +5 -0
- package/package.json +3 -3
- package/src/bin.js +102 -22
- package/src/lib.js +7 -2
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type SubagentFallback,
|
|
40
40
|
} from "./subagents-lib";
|
|
41
41
|
import { childAuthEnv } from "./auth";
|
|
42
|
+
import { sanitizeToolEnvironment } from "./secret-env";
|
|
42
43
|
import { yoloActive } from "./yolo-lib";
|
|
43
44
|
|
|
44
45
|
/** childAuthEnv, never throwing (a spawn must not fail on the auth file). */
|
|
@@ -444,6 +445,8 @@ const ROLE_RE = /^\s*\{\s*"type"\s*:\s*"message_end"\s*,\s*"message"\s*:\s*\{\s*
|
|
|
444
445
|
export class ChildEventScanner {
|
|
445
446
|
finalText = "";
|
|
446
447
|
turns = 0;
|
|
448
|
+
/** Monotonic count of model/tool events that represent real progress. */
|
|
449
|
+
progressSerial = 0;
|
|
447
450
|
readonly toolsRunning = new Set<string>();
|
|
448
451
|
private line = "";
|
|
449
452
|
/** head: deciding from the first characters; keep: buffering the whole line; skip: dropping it. */
|
|
@@ -486,6 +489,7 @@ export class ChildEventScanner {
|
|
|
486
489
|
const head = this.line.slice(0, CHILD_EVENT_HEAD_CHARS);
|
|
487
490
|
const type = EVENT_TYPE_RE.exec(head)?.[1] ?? null;
|
|
488
491
|
this.lineType = type;
|
|
492
|
+
if (type === "message_update" || type === "message_start" || type === "turn_start") this.progressSerial += 1;
|
|
489
493
|
if (type === "message_end") {
|
|
490
494
|
const role = ROLE_RE.exec(head)?.[1] ?? null;
|
|
491
495
|
this.lineRole = role;
|
|
@@ -514,6 +518,7 @@ export class ChildEventScanner {
|
|
|
514
518
|
}
|
|
515
519
|
|
|
516
520
|
private tool(type: string, id: string): void {
|
|
521
|
+
this.progressSerial += 1;
|
|
517
522
|
if (type === "tool_execution_start") this.toolsRunning.add(id);
|
|
518
523
|
else this.toolsRunning.delete(id);
|
|
519
524
|
}
|
|
@@ -530,7 +535,7 @@ export class ChildEventScanner {
|
|
|
530
535
|
if (!trimmed || !trimmed.includes('"type"')) return;
|
|
531
536
|
// A short line: skip the types nothing is read from without parsing them.
|
|
532
537
|
const type = mode === "head" ? EVENT_TYPE_RE.exec(trimmed)?.[1] : undefined;
|
|
533
|
-
if (type && type !== "message_end" && type !== "tool_execution_start" && type !== "tool_execution_end") return;
|
|
538
|
+
if (type && type !== "message_end" && type !== "message_update" && type !== "message_start" && type !== "turn_start" && type !== "tool_execution_start" && type !== "tool_execution_end") return;
|
|
534
539
|
let event: any;
|
|
535
540
|
try {
|
|
536
541
|
event = JSON.parse(trimmed);
|
|
@@ -541,10 +546,15 @@ export class ChildEventScanner {
|
|
|
541
546
|
this.tool(event.type, event.toolCallId);
|
|
542
547
|
return;
|
|
543
548
|
}
|
|
549
|
+
if (event?.type === "message_update" || event?.type === "message_start" || event?.type === "turn_start") {
|
|
550
|
+
this.progressSerial += 1;
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
544
553
|
if (event?.type !== "message_end" || !event.message) return;
|
|
545
554
|
const message = event.message;
|
|
546
555
|
if (message.role !== "assistant") return;
|
|
547
556
|
this.turns += 1;
|
|
557
|
+
this.progressSerial += 1;
|
|
548
558
|
const parts = Array.isArray(message.content) ? message.content : [];
|
|
549
559
|
const text = parts
|
|
550
560
|
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
|
|
@@ -602,7 +612,7 @@ export interface SpawnChildOptions {
|
|
|
602
612
|
now?: () => number;
|
|
603
613
|
/** Progress callback: partial output lines as the child runs. */
|
|
604
614
|
onChildStdout?: (role: AgentRole, chunk: string) => void;
|
|
605
|
-
/**
|
|
615
|
+
/** Meaningful model/tool progress from the child, with its event types. */
|
|
606
616
|
onActivity?: (activity: ChildActivity) => void;
|
|
607
617
|
/** The child's session id (a fresh UUID by default). */
|
|
608
618
|
sessionId?: string;
|
|
@@ -618,6 +628,7 @@ export interface ChildActivity {
|
|
|
618
628
|
turns: number;
|
|
619
629
|
/** Tool calls running right now. */
|
|
620
630
|
toolsRunning: number;
|
|
631
|
+
meaningful: true;
|
|
621
632
|
}
|
|
622
633
|
|
|
623
634
|
/**
|
|
@@ -677,6 +688,7 @@ export async function runChildAgent(
|
|
|
677
688
|
const child = spawnImpl(invocation.command, invocation.args, {
|
|
678
689
|
cwd: options.cwd,
|
|
679
690
|
shell: false,
|
|
691
|
+
detached: process.platform !== "win32",
|
|
680
692
|
stdio: ["ignore", "pipe", "pipe"],
|
|
681
693
|
// Shared credentials by location (OMNIRUSH_DIR), never a token
|
|
682
694
|
// frozen at the parent's launch: the child re-reads auth.json for
|
|
@@ -697,16 +709,33 @@ export async function runChildAgent(
|
|
|
697
709
|
};
|
|
698
710
|
|
|
699
711
|
const killTree = () => {
|
|
700
|
-
|
|
701
|
-
child.
|
|
702
|
-
|
|
703
|
-
|
|
712
|
+
const signalGroup = (signal: NodeJS.Signals) => {
|
|
713
|
+
if (process.platform !== "win32" && typeof child.pid === "number" && child.pid > 0) {
|
|
714
|
+
try {
|
|
715
|
+
process.kill(-child.pid, signal);
|
|
716
|
+
return true;
|
|
717
|
+
} catch {
|
|
718
|
+
/* fall back to the direct child below */
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (process.platform === "win32" && typeof child.pid === "number" && child.pid > 0) {
|
|
722
|
+
try {
|
|
723
|
+
const args = ["/PID", String(child.pid), "/T"];
|
|
724
|
+
if (signal === "SIGKILL") args.push("/F");
|
|
725
|
+
nodeSpawn("taskkill", args, { windowsHide: true, stdio: "ignore" });
|
|
726
|
+
return true;
|
|
727
|
+
} catch {
|
|
728
|
+
/* fall back to the direct child below */
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return false;
|
|
732
|
+
};
|
|
733
|
+
if (!signalGroup("SIGTERM")) {
|
|
734
|
+
try { child.kill("SIGTERM"); } catch { /* already gone */ }
|
|
704
735
|
}
|
|
705
736
|
setTimeout(() => {
|
|
706
|
-
|
|
707
|
-
child.kill("SIGKILL");
|
|
708
|
-
} catch {
|
|
709
|
-
/* already gone */
|
|
737
|
+
if (!signalGroup("SIGKILL")) {
|
|
738
|
+
try { child.kill("SIGKILL"); } catch { /* already gone */ }
|
|
710
739
|
}
|
|
711
740
|
}, Math.max(50, options.killGraceMs ?? CHILD_KILL_GRACE_MS)).unref?.();
|
|
712
741
|
};
|
|
@@ -726,10 +755,11 @@ export async function runChildAgent(
|
|
|
726
755
|
watchdog = setTimeout(() => kill("stalled"), window);
|
|
727
756
|
watchdog.unref?.();
|
|
728
757
|
};
|
|
729
|
-
const alive = () => {
|
|
758
|
+
const alive = (meaningful: boolean) => {
|
|
759
|
+
if (!meaningful) return;
|
|
730
760
|
lastActivity = now();
|
|
731
761
|
armWatchdog();
|
|
732
|
-
options.onActivity?.({ at: lastActivity, turns: events.turns, toolsRunning: toolsRunning.size });
|
|
762
|
+
options.onActivity?.({ at: lastActivity, turns: events.turns, toolsRunning: toolsRunning.size, meaningful: true });
|
|
733
763
|
};
|
|
734
764
|
armWatchdog();
|
|
735
765
|
|
|
@@ -749,8 +779,9 @@ export async function runChildAgent(
|
|
|
749
779
|
child.stdout?.setEncoding?.("utf8");
|
|
750
780
|
child.stdout?.on("data", (chunk: Buffer | string) => {
|
|
751
781
|
const text = String(chunk);
|
|
782
|
+
const before = events.progressSerial;
|
|
752
783
|
events.push(text);
|
|
753
|
-
alive();
|
|
784
|
+
alive(events.progressSerial !== before);
|
|
754
785
|
options.onChildStdout?.(task.role, text);
|
|
755
786
|
});
|
|
756
787
|
child.stderr?.setEncoding?.("utf8");
|
|
@@ -760,10 +791,17 @@ export async function runChildAgent(
|
|
|
760
791
|
const lines = (pendingErrLine + text).split("\n");
|
|
761
792
|
pendingErrLine = lines.pop() ?? "";
|
|
762
793
|
if (pendingErrLine.length > 64 * 1024) pendingErrLine = pendingErrLine.slice(-64 * 1024);
|
|
763
|
-
|
|
794
|
+
let meaningful = false;
|
|
795
|
+
for (const line of lines) {
|
|
796
|
+
const fallback = parseFallbackMarker(line);
|
|
797
|
+
if (fallback) {
|
|
798
|
+
gatewayFallback = fallback;
|
|
799
|
+
meaningful = true;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
764
802
|
stderr += text;
|
|
765
803
|
if (stderr.length > 256 * 1024) stderr = stderr.slice(-64 * 1024);
|
|
766
|
-
alive();
|
|
804
|
+
alive(meaningful);
|
|
767
805
|
});
|
|
768
806
|
child.on("error", (error: Error) => {
|
|
769
807
|
finish({
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
} from "./bgshell-lib";
|
|
43
43
|
import { deliveryHub } from "./deliveries";
|
|
44
44
|
import { BACKGROUND_BASH_TYPE } from "./pi-engine";
|
|
45
|
+
import { sanitizeToolEnvironment } from "./secret-env";
|
|
45
46
|
|
|
46
47
|
/** customType of the message that brings finished background commands back (the trace maps it to bash tool parts). */
|
|
47
48
|
export const BASH_DELIVERY_TYPE = BACKGROUND_BASH_TYPE;
|
|
@@ -221,7 +222,14 @@ export default function (pi: any, options: { exec?: ShellExec; now?: () => numbe
|
|
|
221
222
|
const operations = {
|
|
222
223
|
exec: (command: string, cwd: string, execOptions: any) =>
|
|
223
224
|
new Promise<{ exitCode: number | null }>((resolve, reject) => {
|
|
224
|
-
const current = manager.start({
|
|
225
|
+
const current = manager.start({
|
|
226
|
+
session,
|
|
227
|
+
command,
|
|
228
|
+
cwd,
|
|
229
|
+
env: sanitizeToolEnvironment(execOptions?.env ?? process.env),
|
|
230
|
+
toolCallId,
|
|
231
|
+
onData: execOptions?.onData,
|
|
232
|
+
});
|
|
225
233
|
job = current;
|
|
226
234
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
227
235
|
const onAbort = () => { void manager.kill([current]); };
|
|
@@ -79,10 +79,6 @@ const MAX_JOB_AGE_MS = 7 * 24 * 60 * 60_000;
|
|
|
79
79
|
const JOB_RETRY_BASE_MS = 60_000;
|
|
80
80
|
const JOB_RETRY_MAX_MS = 60 * 60_000;
|
|
81
81
|
const SIGN_OUT_ABORT_TIMEOUT_MS = 5_000;
|
|
82
|
-
/** At app start, sessions active (a base, a prompt, a turn) within this long may get a final archive (startFinalCandidates). */
|
|
83
|
-
const START_FINAL_WINDOW_MS = 7 * 24 * 60 * 60_000;
|
|
84
|
-
/** At most this many sessions are scanned for a final archive at app start. */
|
|
85
|
-
const MAX_START_FINALS = 10;
|
|
86
82
|
/**
|
|
87
83
|
* How long a folder policy answer (4.4: all folders, touched files) is
|
|
88
84
|
* reused for folders without `.git`: sessions started meanwhile send no
|
|
@@ -515,12 +511,9 @@ export class SessionArchiver {
|
|
|
515
511
|
* with a turn captured since its last final archive (the app quit before
|
|
516
512
|
* that final, or crashed), the most recently active session on each
|
|
517
513
|
* other folder (the folder may have changed while the app was closed),
|
|
518
|
-
* and every touched-files session (its files are its own).
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
* archive never extends the window, so a chat left alone gets none a week
|
|
522
|
-
* after its last turn, however often the app starts. The most recently
|
|
523
|
-
* active first, at most 10.
|
|
514
|
+
* and every touched-files session (its files are its own). Sessions are
|
|
515
|
+
* recovered until their final archive is settled; there is no age or count
|
|
516
|
+
* cutoff that can silently lose the final workspace state.
|
|
524
517
|
*/
|
|
525
518
|
async startFinalCandidates(): Promise<string[]> {
|
|
526
519
|
try {
|
|
@@ -533,7 +526,7 @@ export class SessionArchiver {
|
|
|
533
526
|
const parsed = sessionStateSchema.safeParse(await readJsonFile(join(this.dirs.sessions, name)));
|
|
534
527
|
if (!parsed.success || name !== `${stateKey(parsed.data.session_id)}.json`) continue;
|
|
535
528
|
const state = parsed.data;
|
|
536
|
-
if (state.stopped || state.ended ||
|
|
529
|
+
if (state.stopped || state.ended || Date.parse(state.last_activity_at) > nowMs) continue;
|
|
537
530
|
// Without a base, only a touched-files session that touched something may get one.
|
|
538
531
|
if (state.next_sequence === 0 && !(state.marker === TOUCHED_MARKER && (await this.touched.has(state.session_id)))) continue;
|
|
539
532
|
sessions.push(state);
|
|
@@ -548,7 +541,7 @@ export class SessionArchiver {
|
|
|
548
541
|
roots.add(folder);
|
|
549
542
|
if (state.final_due || newestOnRoot) picked.push(state.session_id);
|
|
550
543
|
}
|
|
551
|
-
return picked
|
|
544
|
+
return picked;
|
|
552
545
|
} catch (error) {
|
|
553
546
|
this.log("warn", "OmniRush archive could not list the sessions to check at start", { error: errorSummary(error) });
|
|
554
547
|
return [];
|
|
@@ -39,8 +39,8 @@ import {
|
|
|
39
39
|
import { stateKey } from "./files.js";
|
|
40
40
|
import type { ArchiveLog } from "./upload.js";
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
export const MAX_TOUCHED_PATHS =
|
|
42
|
+
/** Kept as a compatibility export; touched paths are no longer silently capped. */
|
|
43
|
+
export const MAX_TOUCHED_PATHS = Number.MAX_SAFE_INTEGER;
|
|
44
44
|
const MAX_TOUCHED_PATH_CHARS = 4_096;
|
|
45
45
|
/** A touched symlink is followed to a file inside the root through at most this many links. */
|
|
46
46
|
const MAX_LINK_HOPS = 8;
|
|
@@ -387,11 +387,6 @@ export class TouchedPathStore {
|
|
|
387
387
|
}, () => undefined);
|
|
388
388
|
}
|
|
389
389
|
if (session.mode === "ignored" || session.known?.has(path) || session.pending.has(path) || touchedPathParts(path) === null) return;
|
|
390
|
-
if ((session.known?.size ?? 0) + session.pending.size + session.writing.length >= MAX_TOUCHED_PATHS) {
|
|
391
|
-
if (!session.capped) this.options.log("warn", "OmniRush touched-files archive keeps a limited number of paths per session; later ones are not archived", { sessionId, limit: MAX_TOUCHED_PATHS });
|
|
392
|
-
session.capped = true;
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
390
|
session.pending.add(path);
|
|
396
391
|
if (session.mode === "tracked") this.schedule();
|
|
397
392
|
}
|
|
@@ -92,7 +92,6 @@ const MAX_SESSION_LEDGER_ENTRIES = 512;
|
|
|
92
92
|
const SESSION_LEDGER_LOCK_TIMEOUT_MS = 5_000;
|
|
93
93
|
const SESSION_LEDGER_LOCK_STALE_MS = 60_000;
|
|
94
94
|
const SESSION_LEDGER_LOCK_RETRY_MS = 25;
|
|
95
|
-
const MAX_TOUCHED_PATHS = 128;
|
|
96
95
|
const MAX_GIT_STATUS_ENTRIES = 500;
|
|
97
96
|
const MAX_GIT_RECENT_COMMITS = 50;
|
|
98
97
|
const MAX_GIT_REMOTES = 10;
|
|
@@ -4435,7 +4434,7 @@ export class WorkspaceCollector {
|
|
|
4435
4434
|
}
|
|
4436
4435
|
|
|
4437
4436
|
private touchedPathsForUpload(state: SessionState): string[] {
|
|
4438
|
-
return [...state.touchedPaths].
|
|
4437
|
+
return [...state.touchedPaths].map(collectorPathForUpload);
|
|
4439
4438
|
}
|
|
4440
4439
|
|
|
4441
4440
|
/**
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import recorder from "./recorder";
|
|
19
|
+
import recordStreamTiming from "./stream-timing";
|
|
19
20
|
import sota from "./sota";
|
|
20
21
|
import mcp from "./mcp";
|
|
21
22
|
import agents from "./agents";
|
|
@@ -37,6 +38,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
37
38
|
const subagent = isSubagentProcess();
|
|
38
39
|
if (!subagent) (await import("./commands")).default(pi as any);
|
|
39
40
|
recorder(pi as any);
|
|
41
|
+
// Every process times its own runs (the 2.x trace fields); before the
|
|
42
|
+
// collector, so a settled run's idle entry is written before it is read.
|
|
43
|
+
recordStreamTiming(pi as any);
|
|
40
44
|
sota(pi as any);
|
|
41
45
|
if (!subagent) (await import("./collector")).default(pi as any);
|
|
42
46
|
// Children still mark their own session as a sub-agent's (the collector
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
type McpServerConfig,
|
|
44
44
|
} from "./mcp-lib";
|
|
45
45
|
import { truncateTail } from "@earendil-works/pi-coding-agent";
|
|
46
|
+
import { sanitizeToolEnvironment } from "./secret-env";
|
|
46
47
|
|
|
47
48
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
48
49
|
|
|
@@ -218,7 +219,7 @@ async function connectServer(state: ServerState): Promise<void> {
|
|
|
218
219
|
transport = new StdioClientTransport({
|
|
219
220
|
command: state.config.command!,
|
|
220
221
|
args: state.config.args ?? [],
|
|
221
|
-
|
|
222
|
+
env: sanitizeToolEnvironment({ ...process.env, ...(state.config.env ?? {}) }),
|
|
222
223
|
stderr: "pipe",
|
|
223
224
|
});
|
|
224
225
|
}
|