omnirush 0.8.6 → 0.9.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/CHANGELOG.md +120 -0
- package/assets/extensions/omnirush/agents-lib.ts +501 -63
- package/assets/extensions/omnirush/agents.ts +140 -115
- package/assets/extensions/omnirush/bgshell-lib.ts +432 -0
- package/assets/extensions/omnirush/bgshell.ts +392 -0
- package/assets/extensions/omnirush/capture/workspace-collector.ts +86 -22
- package/assets/extensions/omnirush/collector.ts +275 -43
- package/assets/extensions/omnirush/commands.ts +2 -0
- package/assets/extensions/omnirush/deliveries.ts +145 -0
- package/assets/extensions/omnirush/guard/UPSTREAM +2 -0
- package/assets/extensions/omnirush/guard/git-command-policy.ts +885 -0
- package/assets/extensions/omnirush/guard-lib.ts +230 -0
- package/assets/extensions/omnirush/guard.ts +340 -0
- package/assets/extensions/omnirush/index.ts +32 -6
- package/assets/extensions/omnirush/mcp.ts +103 -8
- package/assets/extensions/omnirush/memory-lib.ts +516 -0
- package/assets/extensions/omnirush/pi-engine.ts +115 -3
- package/assets/extensions/omnirush/plan-lib.ts +38 -9
- package/assets/extensions/omnirush/plan.ts +85 -30
- package/assets/extensions/omnirush/sota.ts +52 -4
- package/assets/extensions/omnirush/status-lib.ts +3 -0
- package/assets/extensions/omnirush/subagent-marker.ts +21 -0
- package/assets/extensions/omnirush/subagents-lib.ts +623 -0
- package/assets/extensions/omnirush/subagents.ts +305 -0
- package/assets/extensions/omnirush/swarm-lib.ts +142 -0
- package/assets/extensions/omnirush/swarm.ts +95 -0
- package/assets/extensions/omnirush/voice/capture.ts +502 -0
- package/assets/extensions/omnirush/voice/core/UPSTREAM +16 -0
- package/assets/extensions/omnirush/voice/core/file-source.ts +70 -0
- package/assets/extensions/omnirush/voice/core/index.ts +21 -0
- package/assets/extensions/omnirush/voice/core/keyterms.ts +117 -0
- package/assets/extensions/omnirush/voice/core/resample.ts +63 -0
- package/assets/extensions/omnirush/voice/core/segmenter.ts +231 -0
- package/assets/extensions/omnirush/voice/core/session.ts +403 -0
- package/assets/extensions/omnirush/voice/core/text.ts +81 -0
- package/assets/extensions/omnirush/voice/core/transcriber.ts +135 -0
- package/assets/extensions/omnirush/voice/core/types.ts +102 -0
- package/assets/extensions/omnirush/voice/core/wav.ts +95 -0
- package/assets/extensions/omnirush/voice/keys.ts +435 -0
- package/assets/extensions/omnirush/voice/kitty.ts +64 -0
- package/assets/extensions/omnirush/voice/pvrecorder-worker.cjs +43 -0
- package/assets/extensions/omnirush/voice/settings.ts +67 -0
- package/assets/extensions/omnirush/voice.ts +838 -0
- package/assets/extensions/omnirush/yolo-lib.ts +80 -0
- package/assets/extensions/omnirush/yolo.ts +85 -0
- package/package.json +7 -3
- package/scripts/brand-engine.js +526 -0
- package/scripts/build-all-packages.py +29 -1
- package/scripts/smoke-packages.py +33 -1
- package/src/bin.js +232 -33
- package/src/compat.js +272 -0
- package/src/lib.js +64 -0
- package/src/sessions.js +222 -0
- package/scripts/patch-pi-branding.js +0 -251
|
@@ -32,10 +32,20 @@
|
|
|
32
32
|
// session reaches the parent's trace as session.child events, like the
|
|
33
33
|
// desktop's task sub-agents.
|
|
34
34
|
//
|
|
35
|
+
// A turn's prompt is only what the user submitted. A sub-agent process
|
|
36
|
+
// marks its own session (an `omnirush-subagent` entry naming its parent),
|
|
37
|
+
// so when that session is later opened as a main session (`--continue`,
|
|
38
|
+
// `--resume`, `--session`) its first user message, the task the parent
|
|
39
|
+
// agent wrote, is recorded as synthetic; for sub-agents that wrote no
|
|
40
|
+
// marker, the parent session file naming it in a spawn result confirms
|
|
41
|
+
// it. A prompt an extension sends (pi's input event, source "extension")
|
|
42
|
+
// is synthetic too.
|
|
43
|
+
//
|
|
35
44
|
// Silent no-op without device credentials: collection only runs for
|
|
36
45
|
// signed-in omnirush identities.
|
|
37
46
|
|
|
38
47
|
import { createHash } from "node:crypto";
|
|
48
|
+
import markSubagentSession from "./subagent-marker";
|
|
39
49
|
import { createReadStream, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
40
50
|
import { readdir, stat } from "node:fs/promises";
|
|
41
51
|
import os from "node:os";
|
|
@@ -51,13 +61,17 @@ import {
|
|
|
51
61
|
engineMessagesFromEntries,
|
|
52
62
|
MAX_TURN_MESSAGE_BYTES,
|
|
53
63
|
messageParts,
|
|
64
|
+
subagentTask,
|
|
65
|
+
SUBAGENT_ENTRY_TYPE,
|
|
54
66
|
turnHistory,
|
|
55
67
|
turnModelFromMessages,
|
|
68
|
+
userText,
|
|
56
69
|
type EngineMessage,
|
|
57
70
|
type PiEntry,
|
|
58
71
|
} from "./pi-engine";
|
|
59
72
|
import { sharedRefresher } from "./refresh";
|
|
60
|
-
import { AGENTS_HOOK, type AgentsHook } from "./agents-lib";
|
|
73
|
+
import { AGENTS_HOOK, SUBAGENT_SESSIONS_DIR, type AgentsHook } from "./agents-lib";
|
|
74
|
+
import { BGSHELL_HOOK, type BgShellHook } from "./bgshell-lib";
|
|
61
75
|
|
|
62
76
|
/** info.agent of the root session's messages (a sub-agent's is its role). */
|
|
63
77
|
export const ROOT_AGENT = "omnirush-cli";
|
|
@@ -83,11 +97,12 @@ const SWITCH_BUDGET_MS = 30_000;
|
|
|
83
97
|
*/
|
|
84
98
|
const PRINT_EXIT_GRACE_MS = 2_000;
|
|
85
99
|
|
|
86
|
-
/** The
|
|
100
|
+
/** The agent data dir: session files live under <dir>/sessions (the launcher always sets it). */
|
|
87
101
|
function piAgentDir(): string {
|
|
88
|
-
const override = process.env.
|
|
102
|
+
const override = process.env.OMNIRUSH_CODING_AGENT_DIR || process.env.PI_CODING_AGENT_DIR || "";
|
|
89
103
|
if (override.trim()) return path.resolve(override.trim());
|
|
90
|
-
|
|
104
|
+
const stateDir = (process.env.OMNIRUSH_DIR || "").trim();
|
|
105
|
+
return path.join(stateDir ? path.resolve(stateDir) : path.join(os.homedir(), ".omnirush"), "agent");
|
|
91
106
|
}
|
|
92
107
|
|
|
93
108
|
/** pi session ids are UUIDs; the collector refuses anything outside its id alphabet. */
|
|
@@ -124,26 +139,68 @@ function removeLegacyJournals(stateDir: string): void {
|
|
|
124
139
|
}
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
/**
|
|
128
|
-
export
|
|
129
|
-
|
|
142
|
+
/** A sub-agent that ran on the main model instead of the picked one (spawn_agents' model_fallback). */
|
|
143
|
+
export type ChildModelFallback = { requested: string; used: string; effort: string | null; reason: string; kind: string; note: string };
|
|
144
|
+
|
|
145
|
+
/** Tools and messages that report sub-agent results after spawn_agents named them. */
|
|
146
|
+
const CHILD_RESULT_TOOLS = new Set(["agents_wait", "agents_result", "agents_cancel"]);
|
|
147
|
+
const CHILD_RESULT_MESSAGE = "omnirush-agents-result";
|
|
148
|
+
|
|
149
|
+
function modelFallbackOf(result: any): ChildModelFallback | null {
|
|
150
|
+
const fallback = result && typeof result.model_fallback === "object" ? result.model_fallback : null;
|
|
151
|
+
if (!fallback || typeof fallback.requested !== "string" || typeof fallback.used !== "string") return null;
|
|
152
|
+
return {
|
|
153
|
+
requested: fallback.requested,
|
|
154
|
+
used: fallback.used,
|
|
155
|
+
effort: typeof fallback.effort === "string" ? fallback.effort : null,
|
|
156
|
+
reason: typeof fallback.reason === "string" ? fallback.reason : "refused",
|
|
157
|
+
kind: typeof fallback.kind === "string" ? fallback.kind : "selection",
|
|
158
|
+
note: typeof fallback.note === "string" ? fallback.note : `ran on ${fallback.used}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Sub-agents a session started: its spawn_agents results (session id, role,
|
|
164
|
+
* task), with the model fallback a later result reported (a background
|
|
165
|
+
* child's delivery, agents_wait / agents_result) when there is one.
|
|
166
|
+
*/
|
|
167
|
+
export function spawnedChildren(entries: readonly PiEntry[]): Array<{ id: string; role: string | null; task: string | null; modelFallback?: ChildModelFallback }> {
|
|
168
|
+
const children: Array<{ id: string; role: string | null; task: string | null; modelFallback?: ChildModelFallback }> = [];
|
|
130
169
|
for (const entry of entries) {
|
|
131
170
|
const message = entry.type === "message" ? entry.message : null;
|
|
132
|
-
|
|
133
|
-
const
|
|
171
|
+
const spawned = Boolean(message && message.role === "toolResult" && message.toolName === "spawn_agents");
|
|
172
|
+
const details = spawned
|
|
173
|
+
? message!.details
|
|
174
|
+
: message && message.role === "toolResult" && CHILD_RESULT_TOOLS.has(message.toolName)
|
|
175
|
+
? message.details
|
|
176
|
+
: entry.type === "custom_message" && (entry as any).customType === CHILD_RESULT_MESSAGE
|
|
177
|
+
? (entry as any).details
|
|
178
|
+
: null;
|
|
179
|
+
const results = details && Array.isArray(details.results) ? details.results : [];
|
|
134
180
|
for (const result of results) {
|
|
135
181
|
const id = result && typeof result.session_id === "string" ? result.session_id : "";
|
|
136
|
-
if (!sanitizeSessionId(id)
|
|
182
|
+
if (!sanitizeSessionId(id)) continue;
|
|
183
|
+
const known = children.find((child) => child.id === id);
|
|
184
|
+
const modelFallback = modelFallbackOf(result);
|
|
185
|
+
if (known) {
|
|
186
|
+
if (modelFallback) known.modelFallback = modelFallback;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!spawned) continue;
|
|
137
190
|
children.push({
|
|
138
191
|
id,
|
|
139
192
|
role: typeof result.role === "string" ? result.role : null,
|
|
140
193
|
task: typeof result.task === "string" ? result.task : null,
|
|
194
|
+
...(modelFallback ? { modelFallback } : {}),
|
|
141
195
|
});
|
|
142
196
|
}
|
|
143
197
|
}
|
|
144
198
|
return children;
|
|
145
199
|
}
|
|
146
200
|
|
|
201
|
+
/** Fallbacks already recorded as trace events (root session : child session). */
|
|
202
|
+
const recordedFallbacks = new Set<string>();
|
|
203
|
+
|
|
147
204
|
/** The session file of `sessionId`: `<ts>_<id>.jsonl` in one of the session dirs. */
|
|
148
205
|
async function findSessionFile(sessionId: string, dirs: readonly string[]): Promise<string | null> {
|
|
149
206
|
for (const dir of dirs) {
|
|
@@ -159,6 +216,71 @@ async function findSessionFile(sessionId: string, dirs: readonly string[]): Prom
|
|
|
159
216
|
return null;
|
|
160
217
|
}
|
|
161
218
|
|
|
219
|
+
/** Session files read at most when looking for a sub-agent's parent, and for how long. */
|
|
220
|
+
const MAX_PARENT_SCAN_FILES = 64;
|
|
221
|
+
const PARENT_SCAN_BUDGET_MS = 3_000;
|
|
222
|
+
|
|
223
|
+
/** Whether `file` contains `needle` (streamed in chunks, never held whole). */
|
|
224
|
+
async function fileContains(file: string, needle: string, deadline: number): Promise<boolean> {
|
|
225
|
+
const input = createReadStream(file, { encoding: "utf8", highWaterMark: 256 * 1024 });
|
|
226
|
+
let tail = "";
|
|
227
|
+
try {
|
|
228
|
+
for await (const chunk of input) {
|
|
229
|
+
const text = tail + (chunk as string);
|
|
230
|
+
if (text.includes(needle)) return true;
|
|
231
|
+
if (Date.now() > deadline) return false;
|
|
232
|
+
tail = text.slice(-needle.length);
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
/* unreadable: not the parent */
|
|
236
|
+
} finally {
|
|
237
|
+
input.destroy();
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The session that started sub-agent `childId`: the session file in `dir`
|
|
244
|
+
* whose spawn_agents (or agents_*) results name it, among the files
|
|
245
|
+
* written since the sub-agent started (`since`, epoch ms; the newest
|
|
246
|
+
* first, at most MAX_PARENT_SCAN_FILES, within PARENT_SCAN_BUDGET_MS).
|
|
247
|
+
*/
|
|
248
|
+
export async function findSpawningSession(childId: string, dir: string, since: number): Promise<string | null> {
|
|
249
|
+
const deadline = Date.now() + PARENT_SCAN_BUDGET_MS;
|
|
250
|
+
const found = await findSpawningSessionIn(childId, dir, since, deadline);
|
|
251
|
+
if (found) return found;
|
|
252
|
+
// A sub-agent session moved into <session dir>/subagents/ (src/sessions.js,
|
|
253
|
+
// or written there by a newer parent): its parent sits one level up.
|
|
254
|
+
if (path.basename(dir) === SUBAGENT_SESSIONS_DIR) {
|
|
255
|
+
return findSpawningSessionIn(childId, path.dirname(dir), since, deadline);
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function findSpawningSessionIn(childId: string, dir: string, since: number, deadline: number): Promise<string | null> {
|
|
261
|
+
let names: string[];
|
|
262
|
+
try {
|
|
263
|
+
names = await readdir(dir);
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const candidates: Array<{ file: string; id: string; mtime: number }> = [];
|
|
268
|
+
for (const name of names) {
|
|
269
|
+
const match = /_([A-Za-z0-9._:-]{8,128})\.jsonl$/.exec(name);
|
|
270
|
+
if (!match || match[1] === childId) continue;
|
|
271
|
+
const file = path.join(dir, name);
|
|
272
|
+
const stats = await stat(file).catch(() => null);
|
|
273
|
+
if (stats && stats.mtimeMs >= since) candidates.push({ file, id: match[1]!, mtime: stats.mtimeMs });
|
|
274
|
+
}
|
|
275
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
276
|
+
const needle = `"session_id":${JSON.stringify(childId)}`;
|
|
277
|
+
for (const candidate of candidates.slice(0, MAX_PARENT_SCAN_FILES)) {
|
|
278
|
+
if (Date.now() > deadline) break;
|
|
279
|
+
if (await fileContains(candidate.file, needle, deadline)) return candidate.id;
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
162
284
|
/** Parse a session JSONL file without buffering the raw transcript or its line array. */
|
|
163
285
|
export async function parseSessionFileStream(file: string): Promise<{ header: PiEntry | null; entries: PiEntry[] }> {
|
|
164
286
|
let header: PiEntry | null = null;
|
|
@@ -209,7 +331,8 @@ export async function captureChildSessions(input: {
|
|
|
209
331
|
visited: Set<string>;
|
|
210
332
|
/** Unchanged session files are not read again in this process. */
|
|
211
333
|
seenFiles: Map<string, string>;
|
|
212
|
-
|
|
334
|
+
/** Before each event is recorded (may wait: the caller flushes and drains the upload queue). */
|
|
335
|
+
beforeRecord: (bytes: number) => void | Promise<void>;
|
|
213
336
|
}): Promise<void> {
|
|
214
337
|
if (input.depth > MAX_CHILD_DEPTH) return;
|
|
215
338
|
for (const child of spawnedChildren(input.parentEntries).slice(0, MAX_CHILDREN)) {
|
|
@@ -233,13 +356,26 @@ export async function captureChildSessions(input: {
|
|
|
233
356
|
if (history.omitted > 0) {
|
|
234
357
|
input.collector.recordTrace(input.rootSessionId, "session.messages_omitted", { child_session_id: child.id, count: history.omitted });
|
|
235
358
|
}
|
|
359
|
+
const fallbackKey = `${input.rootSessionId}:${child.id}`;
|
|
360
|
+
if (child.modelFallback && !recordedFallbacks.has(fallbackKey)) {
|
|
361
|
+
// As the desktop: the sub-agent ran on the main model, and why.
|
|
362
|
+
recordedFallbacks.add(fallbackKey);
|
|
363
|
+
input.collector.recordTrace(input.rootSessionId, "subagent.model_fallback", {
|
|
364
|
+
child_session_id: child.id,
|
|
365
|
+
requested: child.modelFallback.requested,
|
|
366
|
+
used: child.modelFallback.used,
|
|
367
|
+
effort: child.modelFallback.effort,
|
|
368
|
+
reason: child.modelFallback.reason,
|
|
369
|
+
kind: child.modelFallback.kind,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
236
372
|
if (history.delta.length > 0 || !input.known.has(child.id)) {
|
|
237
|
-
input.beforeRecord(history.sizes.reduce((total, bytes) => total + bytes, 0));
|
|
373
|
+
await input.beforeRecord(history.sizes.reduce((total, bytes) => total + bytes, 0));
|
|
238
374
|
input.collector.recordChildSession(input.rootSessionId, {
|
|
239
375
|
childSessionId: child.id,
|
|
240
376
|
parentSessionId: input.parentSessionId,
|
|
241
377
|
depth: input.depth,
|
|
242
|
-
title: child.task,
|
|
378
|
+
title: child.task && child.modelFallback ? `${child.task} (${child.modelFallback.note})` : child.task,
|
|
243
379
|
agent: turnModelFromMessages(messages)?.agent ?? agent,
|
|
244
380
|
messages: history.delta,
|
|
245
381
|
lastMessageId: typeof messages.at(-1)?.info.id === "string" ? (messages.at(-1)!.info.id as string) : null,
|
|
@@ -252,7 +388,13 @@ export async function captureChildSessions(input: {
|
|
|
252
388
|
|
|
253
389
|
export default function (pi: any) {
|
|
254
390
|
// A sub-agent: the parent session captures it (session.child events).
|
|
255
|
-
|
|
391
|
+
const parentSession = (process.env.OMNIRUSH_PARENT_SESSION || "").trim();
|
|
392
|
+
if (parentSession) {
|
|
393
|
+
// Its session says so (subagent-marker.ts; index.ts loads it for
|
|
394
|
+
// sub-agent processes, which do not load the collector).
|
|
395
|
+
markSubagentSession(pi, parentSession);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
256
398
|
const origin = resolveOrigin(process.env);
|
|
257
399
|
const gatewayUrl = process.env.OMNIRUSH_GATEWAY_URL || gatewayUrlForOrigin(origin);
|
|
258
400
|
// The shared auth file is the source of truth (another process may have
|
|
@@ -319,7 +461,7 @@ export default function (pi: any) {
|
|
|
319
461
|
// value is a bare release number); client_version / client_platform are
|
|
320
462
|
// the backend's optional envelope metadata.
|
|
321
463
|
appVersion: version ? `omnirush-cli/${version}` : "omnirush-cli",
|
|
322
|
-
...(engineVersion ? { engineVersion: `
|
|
464
|
+
...(engineVersion ? { engineVersion: `omnirush-core/${engineVersion}` } : {}),
|
|
323
465
|
envelopeMetadata: {
|
|
324
466
|
...(version ? { client_version: version } : {}),
|
|
325
467
|
client_platform: `${process.platform}/${process.arch}`,
|
|
@@ -372,21 +514,34 @@ export default function (pi: any) {
|
|
|
372
514
|
pendingPrompts: Array<{ at: number; model: { providerID: string; modelID: string } | null; variant: string | null }>;
|
|
373
515
|
/** User entry id -> what its prompt was sent with (pi-engine promptSettings). */
|
|
374
516
|
promptSettings: Map<string, { model: { providerID: string; modelID: string } | null; variant: string | null }>;
|
|
517
|
+
/** Prompts an extension sent (pi's input event, source "extension"), until their user entry is found. */
|
|
518
|
+
extensionInputs: Array<{ at: number; text: string }>;
|
|
519
|
+
/** User entries the user did not submit -> their parts' metadata (pi-engine injectedPrompts). */
|
|
520
|
+
injectedPrompts: Map<string, Record<string, unknown>>;
|
|
521
|
+
/** Whether the session is a sub-agent's (its first prompt the parent agent's task), once looked up. */
|
|
522
|
+
origin: Promise<void> | null;
|
|
375
523
|
} | null = null;
|
|
376
524
|
let settling: Promise<void> = Promise.resolve();
|
|
377
525
|
|
|
526
|
+
// Where sub-agent session files are looked up: the session's own
|
|
527
|
+
// subagents/ folder first (where spawn_agents puts them, and where the
|
|
528
|
+
// launcher moves older releases' children), then the session dirs
|
|
529
|
+
// themselves (children of releases up to 0.9.0 not moved yet), then
|
|
530
|
+
// every project's.
|
|
378
531
|
const sessionDirs = (ctx: any): string[] => {
|
|
379
532
|
const dirs: string[] = [];
|
|
533
|
+
const add = (dir: unknown) => {
|
|
534
|
+
if (typeof dir !== "string" || !dir) return;
|
|
535
|
+
for (const candidate of [path.join(dir, SUBAGENT_SESSIONS_DIR), dir]) {
|
|
536
|
+
if (!dirs.includes(candidate)) dirs.push(candidate);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
380
539
|
const file = ctx?.sessionManager?.getSessionFile?.();
|
|
381
|
-
if (typeof file === "string" && file)
|
|
382
|
-
|
|
383
|
-
if (typeof sessionDir === "string" && sessionDir && !dirs.includes(sessionDir)) dirs.push(sessionDir);
|
|
540
|
+
if (typeof file === "string" && file) add(path.dirname(file));
|
|
541
|
+
add(ctx?.sessionManager?.getSessionDir?.());
|
|
384
542
|
try {
|
|
385
543
|
const root = path.join(agentDir, "sessions");
|
|
386
|
-
for (const name of readdirSync(root))
|
|
387
|
-
const dir = path.join(root, name);
|
|
388
|
-
if (!dirs.includes(dir)) dirs.push(dir);
|
|
389
|
-
}
|
|
544
|
+
for (const name of readdirSync(root)) add(path.join(root, name));
|
|
390
545
|
} catch {
|
|
391
546
|
/* no sessions yet */
|
|
392
547
|
}
|
|
@@ -406,7 +561,23 @@ export default function (pi: any) {
|
|
|
406
561
|
// never wrote a user entry (handled by an extension) is let go.
|
|
407
562
|
else if (pending.at > Date.now() - 60_000) state.pendingPrompts.push(pending);
|
|
408
563
|
}
|
|
409
|
-
|
|
564
|
+
// An extension's prompt is the first user entry with its text written at or after it went out.
|
|
565
|
+
for (const input of state.extensionInputs.splice(0)) {
|
|
566
|
+
const entry = entries.find((candidate: any) =>
|
|
567
|
+
candidate?.type === "message" && candidate.message?.role === "user" && typeof candidate.id === "string"
|
|
568
|
+
&& !state.injectedPrompts.has(candidate.id) && userText(candidate.message) === input.text
|
|
569
|
+
&& Number(candidate.message.timestamp ?? Date.parse(candidate.timestamp)) >= input.at - 1_000);
|
|
570
|
+
if (entry) state.injectedPrompts.set(entry.id, { source: "extension" });
|
|
571
|
+
else if (input.at > Date.now() - 60_000) state.extensionInputs.push(input);
|
|
572
|
+
}
|
|
573
|
+
return engineMessagesFromEntries(entries, {
|
|
574
|
+
sessionId: state.id,
|
|
575
|
+
agent: ROOT_AGENT,
|
|
576
|
+
cwd: state.root,
|
|
577
|
+
toolStarts: state.toolStarts,
|
|
578
|
+
promptSettings: state.promptSettings,
|
|
579
|
+
injectedPrompts: state.injectedPrompts,
|
|
580
|
+
});
|
|
410
581
|
};
|
|
411
582
|
|
|
412
583
|
/**
|
|
@@ -419,7 +590,21 @@ export default function (pi: any) {
|
|
|
419
590
|
if (!id) return null;
|
|
420
591
|
if (session?.id !== id) {
|
|
421
592
|
const root = ctx?.cwd || process.cwd();
|
|
422
|
-
session = {
|
|
593
|
+
session = {
|
|
594
|
+
id,
|
|
595
|
+
root,
|
|
596
|
+
ctx,
|
|
597
|
+
started: false,
|
|
598
|
+
running: false,
|
|
599
|
+
lastMessageId: undefined,
|
|
600
|
+
toolStarts: new Map(),
|
|
601
|
+
seenFiles: new Map(),
|
|
602
|
+
pendingPrompts: [],
|
|
603
|
+
promptSettings: new Map(),
|
|
604
|
+
extensionInputs: [],
|
|
605
|
+
injectedPrompts: new Map(),
|
|
606
|
+
origin: null,
|
|
607
|
+
};
|
|
423
608
|
}
|
|
424
609
|
session.ctx = ctx;
|
|
425
610
|
// Idempotent: a session already started in this process keeps its segment.
|
|
@@ -435,7 +620,7 @@ export default function (pi: any) {
|
|
|
435
620
|
* sub-agents keep running past the turn that started them: each later
|
|
436
621
|
* settle (and the session's end) records what they did since.
|
|
437
622
|
*/
|
|
438
|
-
const captureChildren = async (state: NonNullable<typeof session>, room: (bytes: number) => void): Promise<void> => {
|
|
623
|
+
const captureChildren = async (state: NonNullable<typeof session>, room: (bytes: number) => void | Promise<void>): Promise<void> => {
|
|
439
624
|
const { id } = state;
|
|
440
625
|
try {
|
|
441
626
|
await captureChildSessions({
|
|
@@ -456,6 +641,26 @@ export default function (pi: any) {
|
|
|
456
641
|
}
|
|
457
642
|
};
|
|
458
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Room for the next sub-agent event: once the recorded messages would pass
|
|
646
|
+
* one flush (MAX_TURN_MESSAGE_BYTES), they flush, and the next child is
|
|
647
|
+
* read only after that upload went out. A turn with many sub-agents (20
|
|
648
|
+
* children, MiBs each) used to read and queue all of them at once, holding
|
|
649
|
+
* every child's messages, their trace documents and envelopes in memory
|
|
650
|
+
* together.
|
|
651
|
+
*/
|
|
652
|
+
const childRoom = (id: string) => {
|
|
653
|
+
let pending = 0;
|
|
654
|
+
return async (bytes: number): Promise<void> => {
|
|
655
|
+
if (pending > 0 && pending + bytes > MAX_TURN_MESSAGE_BYTES) {
|
|
656
|
+
collector.flushTrace(id);
|
|
657
|
+
pending = 0;
|
|
658
|
+
await collector.drained(id);
|
|
659
|
+
}
|
|
660
|
+
pending += bytes;
|
|
661
|
+
};
|
|
662
|
+
};
|
|
663
|
+
|
|
459
664
|
/**
|
|
460
665
|
* Everything a settled run records, in the desktop observer's order:
|
|
461
666
|
* messages, sub-agents, the idle event, the snapshot, the trace and the
|
|
@@ -483,22 +688,17 @@ export default function (pi: any) {
|
|
|
483
688
|
for (const [index, part] of parts.slice(0, -1).entries()) {
|
|
484
689
|
collector.recordTrace(id, "turn.messages", { messages: part.messages, part: index + 1, parts: parts.length });
|
|
485
690
|
collector.flushTrace(id);
|
|
691
|
+
// One part waits in memory at a time (a resumed session's backlog is up to 32 MiB).
|
|
692
|
+
await collector.drained(id);
|
|
486
693
|
}
|
|
487
694
|
} catch (error) {
|
|
488
695
|
collector.recordTrace(id, "session.messages_failed", { error: error instanceof Error ? error.message : "unknown" });
|
|
489
696
|
}
|
|
490
697
|
const newest = parts.at(-1)!;
|
|
491
698
|
// Sub-agent messages flush on their own before they would crowd the turn's out.
|
|
492
|
-
|
|
493
|
-
const room = (bytes: number) => {
|
|
494
|
-
if (pending > 0 && pending + bytes > MAX_TURN_MESSAGE_BYTES) {
|
|
495
|
-
collector.flushTrace(id);
|
|
496
|
-
pending = 0;
|
|
497
|
-
}
|
|
498
|
-
pending += bytes;
|
|
499
|
-
};
|
|
699
|
+
const room = childRoom(id);
|
|
500
700
|
await captureChildren(state, room);
|
|
501
|
-
room(newest.bytes);
|
|
701
|
+
await room(newest.bytes);
|
|
502
702
|
collector.recordTrace(id, "session.idle", { status });
|
|
503
703
|
// The turn snapshot runs first so the artifacts it discovers are part of
|
|
504
704
|
// the trace flushed right behind it.
|
|
@@ -527,6 +727,10 @@ export default function (pi: any) {
|
|
|
527
727
|
const hook = (globalThis as any)[AGENTS_HOOK] as AgentsHook | undefined;
|
|
528
728
|
const followed = session;
|
|
529
729
|
const interrupted = followed?.started && hook ? await hook.interruptAll(followed.id).catch(() => []) : [];
|
|
730
|
+
// Background shell commands still running are stopped too; their output
|
|
731
|
+
// is added to the session as a message, so the last capture keeps it.
|
|
732
|
+
const shell = (globalThis as any)[BGSHELL_HOOK] as BgShellHook | undefined;
|
|
733
|
+
if (followed?.started && shell) await shell.stopAll(followed.id).catch(() => []);
|
|
530
734
|
await settleRunning("interrupted");
|
|
531
735
|
const state = session;
|
|
532
736
|
if (!state?.started) return;
|
|
@@ -537,14 +741,7 @@ export default function (pi: any) {
|
|
|
537
741
|
if (typeof lastId !== "string" || lastId === state.lastMessageId) {
|
|
538
742
|
// No new message of its own: what its sub-agents did since the last settle.
|
|
539
743
|
settling = settling.then(async () => {
|
|
540
|
-
|
|
541
|
-
await captureChildren(state, (bytes) => {
|
|
542
|
-
if (pending > 0 && pending + bytes > MAX_TURN_MESSAGE_BYTES) {
|
|
543
|
-
collector.flushTrace(state.id);
|
|
544
|
-
pending = 0;
|
|
545
|
-
}
|
|
546
|
-
pending += bytes;
|
|
547
|
-
});
|
|
744
|
+
await captureChildren(state, childRoom(state.id));
|
|
548
745
|
collector.flushTrace(state.id);
|
|
549
746
|
}).catch(() => undefined);
|
|
550
747
|
await settling;
|
|
@@ -588,11 +785,45 @@ export default function (pi: any) {
|
|
|
588
785
|
clearTimeout(budget);
|
|
589
786
|
};
|
|
590
787
|
|
|
788
|
+
/**
|
|
789
|
+
* A session opened as a main session that is a sub-agent's own (its file
|
|
790
|
+
* sits with the main sessions, so `--continue` or the resume picker can
|
|
791
|
+
* reopen it): its first prompt is the task the parent agent wrote, not
|
|
792
|
+
* something the user typed. Looked up once, before the first prompt of
|
|
793
|
+
* this process is written (every user entry then on the branch came
|
|
794
|
+
* before this process).
|
|
795
|
+
*/
|
|
796
|
+
const lookUpOrigin = (state: NonNullable<typeof session>): Promise<void> =>
|
|
797
|
+
(state.origin ??= (async () => {
|
|
798
|
+
const entries: PiEntry[] = state.ctx?.sessionManager?.getBranch?.() ?? [];
|
|
799
|
+
const task = subagentTask(entries);
|
|
800
|
+
if (!task) return;
|
|
801
|
+
let parent = task.parentSessionId;
|
|
802
|
+
if (!task.marked) {
|
|
803
|
+
// Sub-agents that wrote no marker: the parent session names it.
|
|
804
|
+
const file = state.ctx?.sessionManager?.getSessionFile?.();
|
|
805
|
+
const started = Date.parse(String(state.ctx?.sessionManager?.getHeader?.()?.timestamp ?? entries[0]?.timestamp ?? ""));
|
|
806
|
+
parent = typeof file === "string" && file
|
|
807
|
+
? await findSpawningSession(state.id, path.dirname(file), Number.isFinite(started) ? started - 10 * 60_000 : 0).catch(() => null)
|
|
808
|
+
: null;
|
|
809
|
+
if (!parent) return;
|
|
810
|
+
}
|
|
811
|
+
state.injectedPrompts.set(task.entryId, { subagentTask: true, ...(parent ? { parentSessionID: parent } : {}) });
|
|
812
|
+
})().catch(() => undefined));
|
|
813
|
+
|
|
591
814
|
pi.on("session_start", (_event: any, ctx: any) => {
|
|
592
815
|
follow(ctx, false);
|
|
593
816
|
});
|
|
594
817
|
|
|
595
|
-
|
|
818
|
+
// What the user submits comes as input; an extension's prompt
|
|
819
|
+
// (pi.sendUserMessage) is marked so it never reads as the user's.
|
|
820
|
+
pi.on("input", (event: any, ctx: any) => {
|
|
821
|
+
if (event?.source !== "extension" || typeof event?.text !== "string") return;
|
|
822
|
+
const state = follow(ctx, false);
|
|
823
|
+
state?.extensionInputs.push({ at: Date.now(), text: event.text });
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
pi.on("before_agent_start", async (event: any, ctx: any) => {
|
|
596
827
|
const state = follow(ctx, true);
|
|
597
828
|
if (!state) return;
|
|
598
829
|
state.running = true;
|
|
@@ -649,6 +880,7 @@ export default function (pi: any) {
|
|
|
649
880
|
},
|
|
650
881
|
});
|
|
651
882
|
archive.turnFollowed(state.id);
|
|
883
|
+
await lookUpOrigin(state);
|
|
652
884
|
});
|
|
653
885
|
|
|
654
886
|
// A run that no prompt started (a background sub-agent's results
|
|
@@ -21,6 +21,7 @@ import { readSessionLedger, SESSION_LEDGER_FILE } from "./capture/workspace-coll
|
|
|
21
21
|
import { deviceMe, omniDir, resolveOrigin, sessionEndedMessage } from "./auth";
|
|
22
22
|
import { sharedRefresher } from "./refresh";
|
|
23
23
|
import { formatCollectorLine, formatStatusLines } from "./status-lib";
|
|
24
|
+
import { formatModeStatus } from "./yolo-lib";
|
|
24
25
|
import {
|
|
25
26
|
formatDeviceLines,
|
|
26
27
|
formatGrantLines,
|
|
@@ -153,6 +154,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
153
154
|
collector: { sessionId, record },
|
|
154
155
|
origin: resolveOrigin(process.env),
|
|
155
156
|
version: CLI_VERSION,
|
|
157
|
+
yolo: formatModeStatus(),
|
|
156
158
|
});
|
|
157
159
|
notify(ctx, ["Omnirush status:", ...statusLines.map((line) => ` ${line}`)].join("\n"));
|
|
158
160
|
},
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// deliveries — brings results of background work back into the conversation.
|
|
2
|
+
//
|
|
3
|
+
// Background sub-agents (agents.ts) and background shell commands
|
|
4
|
+
// (bgshell.ts) finish while the main agent works on, waits for the user, or
|
|
5
|
+
// is about to end a one-shot run. Each registers as a source here; one hub
|
|
6
|
+
// per extension instance delivers what they have:
|
|
7
|
+
//
|
|
8
|
+
// - the agent is busy: the result is steered into the running turn (the
|
|
9
|
+
// model sees it before its next step);
|
|
10
|
+
// - the agent is idle: the result starts a new turn;
|
|
11
|
+
// - one-shot runs (print/json mode) end when the agent settles, so the hub
|
|
12
|
+
// holds the run open while a source still has work out, delivers each
|
|
13
|
+
// result as it comes (a new run nested in this settle, whose own settle
|
|
14
|
+
// resumes this one), and gives each source one last word (`beforeEnd`)
|
|
15
|
+
// before the run is allowed to end.
|
|
16
|
+
|
|
17
|
+
export interface DeliveryItem {
|
|
18
|
+
customType: string;
|
|
19
|
+
content: string;
|
|
20
|
+
details?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DeliverySource {
|
|
24
|
+
/** Work of this session whose result the run should still wait for (running, or finished and unread). */
|
|
25
|
+
hasPending(session: string): boolean;
|
|
26
|
+
/** Finished results ready now (each becomes one message); marked delivered. */
|
|
27
|
+
take(session: string): DeliveryItem[];
|
|
28
|
+
/** Resolves at the next change (something finished). */
|
|
29
|
+
changed(): Promise<void>;
|
|
30
|
+
/** One-shot runs with nothing pending: a last message before the run ends, or null. */
|
|
31
|
+
beforeEnd?(session: string): DeliveryItem | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class DeliveryHub {
|
|
35
|
+
private readonly pi: any;
|
|
36
|
+
private readonly sources: DeliverySource[] = [];
|
|
37
|
+
/** The session this runtime is on (deliveries go to it). */
|
|
38
|
+
currentSession = "";
|
|
39
|
+
cwd = process.cwd();
|
|
40
|
+
/** An agent run is under way (one-shot runs deliver through the settle loop otherwise). */
|
|
41
|
+
private busy = false;
|
|
42
|
+
private oneShot = false;
|
|
43
|
+
private resumeOuter: (() => void) | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(pi: any) {
|
|
46
|
+
this.pi = pi;
|
|
47
|
+
pi.on("session_start", (_event: any, ctx: any) => {
|
|
48
|
+
this.sessionOf(ctx);
|
|
49
|
+
});
|
|
50
|
+
pi.on("agent_start", (_event: any, ctx: any) => {
|
|
51
|
+
this.sessionOf(ctx);
|
|
52
|
+
this.busy = true;
|
|
53
|
+
});
|
|
54
|
+
pi.on("agent_settled", (_event: any, ctx: any) => this.settled(ctx));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
addSource(source: DeliverySource): void {
|
|
58
|
+
this.sources.push(source);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A source has something to deliver (called after its own bookkeeping). */
|
|
62
|
+
notify(): void {
|
|
63
|
+
queueMicrotask(() => this.deliver());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The session of a tool or event context (and what the hub learns from it). */
|
|
67
|
+
sessionOf(ctx: any): string {
|
|
68
|
+
const id = String(ctx?.sessionManager?.getSessionId?.() ?? "");
|
|
69
|
+
if (id) this.currentSession = id;
|
|
70
|
+
if (ctx?.cwd) this.cwd = String(ctx.cwd);
|
|
71
|
+
if (ctx?.mode === "print" || ctx?.mode === "json") this.oneShot = true;
|
|
72
|
+
return id;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get isOneShot(): boolean {
|
|
76
|
+
return this.oneShot;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private send(item: DeliveryItem): void {
|
|
80
|
+
this.pi.sendMessage(
|
|
81
|
+
{ customType: item.customType, content: item.content, display: true, ...(item.details !== undefined ? { details: item.details } : {}) },
|
|
82
|
+
// Busy: into the running turn at its next step. Idle: a new turn.
|
|
83
|
+
{ triggerTurn: true, deliverAs: "steer" },
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private deliver(): void {
|
|
88
|
+
if (!this.currentSession) return;
|
|
89
|
+
// One-shot and between runs: the agent_settled loop delivers.
|
|
90
|
+
if (this.oneShot && !this.busy) return;
|
|
91
|
+
for (const source of this.sources) {
|
|
92
|
+
for (const item of source.take(this.currentSession)) this.send(item);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async settled(ctx: any): Promise<void> {
|
|
97
|
+
this.busy = false;
|
|
98
|
+
const session = this.sessionOf(ctx);
|
|
99
|
+
if (!this.oneShot) {
|
|
100
|
+
// Anything that finished right as the run ended.
|
|
101
|
+
this.deliver();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const outer = this.resumeOuter;
|
|
105
|
+
this.resumeOuter = null;
|
|
106
|
+
const idle = () => typeof ctx?.isIdle !== "function" || ctx.isIdle();
|
|
107
|
+
try {
|
|
108
|
+
for (;;) {
|
|
109
|
+
if (!idle()) {
|
|
110
|
+
// A run started (a delivery): its settle resumes this one.
|
|
111
|
+
await new Promise<void>((resolve) => { this.resumeOuter = resolve; });
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
if (!session) break;
|
|
115
|
+
const items = this.sources.flatMap((source) => source.take(session));
|
|
116
|
+
if (items.length === 0 && this.sources.some((source) => source.hasPending(session))) {
|
|
117
|
+
await Promise.race(this.sources.map((source) => source.changed()));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (items.length === 0) {
|
|
121
|
+
const last = this.sources.map((source) => source.beforeEnd?.(session) ?? null).find((item) => item !== null);
|
|
122
|
+
if (!last) break;
|
|
123
|
+
items.push(last);
|
|
124
|
+
}
|
|
125
|
+
this.busy = true;
|
|
126
|
+
for (const item of items) this.send(item);
|
|
127
|
+
if (idle()) this.busy = false;
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
outer?.();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const hubs = new WeakMap<object, DeliveryHub>();
|
|
136
|
+
|
|
137
|
+
/** The hub of this extension instance (created, with its handlers, on first use). */
|
|
138
|
+
export function deliveryHub(pi: any): DeliveryHub {
|
|
139
|
+
let hub = hubs.get(pi);
|
|
140
|
+
if (!hub) {
|
|
141
|
+
hub = new DeliveryHub(pi);
|
|
142
|
+
hubs.set(pi, hub);
|
|
143
|
+
}
|
|
144
|
+
return hub;
|
|
145
|
+
}
|