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.
- package/assets/extensions/omnirush/agents-lib.ts +18 -6
- package/assets/extensions/omnirush/agents.ts +7 -4
- package/assets/extensions/omnirush/auth.js +3 -2
- package/assets/extensions/omnirush/bgshell.ts +6 -6
- package/assets/extensions/omnirush/capture/UPSTREAM +7 -4
- package/assets/extensions/omnirush/capture/omnirush-swarm.ts +2 -2
- package/assets/extensions/omnirush/capture/server-fetch.ts +1 -1
- package/assets/extensions/omnirush/capture/session-archive/detect.ts +3 -3
- package/assets/extensions/omnirush/capture/session-archive/files.ts +1 -1
- package/assets/extensions/omnirush/capture/session-archive/ignored.ts +2 -2
- package/assets/extensions/omnirush/capture/session-archive/index.ts +8 -8
- package/assets/extensions/omnirush/capture/session-archive/lifecycle.ts +1 -1
- package/assets/extensions/omnirush/capture/session-archive/manifest.ts +4 -4
- package/assets/extensions/omnirush/capture/session-archive/touched.ts +2 -2
- package/assets/extensions/omnirush/capture/session-archive/upload.ts +3 -3
- package/assets/extensions/omnirush/capture/{collect-upload-budget.ts → sync-upload-budget.ts} +5 -5
- package/assets/extensions/omnirush/capture/turn-diff.ts +6 -6
- package/assets/extensions/omnirush/capture/{workspace-collector.ts → workspace-sync.ts} +253 -204
- package/assets/extensions/omnirush/commands.ts +6 -6
- package/assets/extensions/omnirush/{pi-engine.ts → engine-messages.ts} +7 -7
- package/assets/extensions/omnirush/index.ts +4 -4
- package/assets/extensions/omnirush/plan.ts +2 -2
- package/assets/extensions/omnirush/refresh.ts +1 -1
- package/assets/extensions/omnirush/retry.js +1 -1
- package/assets/extensions/omnirush/{collector.ts → session-sync.ts} +54 -54
- package/assets/extensions/omnirush/status-lib.ts +9 -9
- package/assets/extensions/omnirush/stream-timing.ts +4 -4
- package/assets/extensions/omnirush/subagent-marker.ts +4 -4
- package/assets/extensions/omnirush/swarm-lib.ts +2 -2
- package/assets/extensions/omnirush/voice.ts +1 -1
- package/assets/{collect-once.ts → sync-once.ts} +20 -19
- package/package.json +1 -1
- package/src/bin.js +20 -8
- package/src/sessions.js +3 -3
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* /usage full usage from the manager (grant date, token counters,
|
|
6
6
|
* devices)
|
|
7
7
|
* /status one-screen session summary (identity, model/effort, grant,
|
|
8
|
-
*
|
|
8
|
+
* session uploader, origin, version)
|
|
9
9
|
* /verify prove the served model is genuine — full implementation
|
|
10
10
|
* lands with the gateway's model-attestation endpoint (M2)
|
|
11
11
|
* plus the footer status line that keeps grant/model info visible while
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
* which is exactly how /grant and /verify broke the first time.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { readSessionLedger, SESSION_LEDGER_FILE } from "./capture/workspace-
|
|
20
|
+
import { readSessionLedger, SESSION_LEDGER_FILE } from "./capture/workspace-sync";
|
|
21
21
|
import { deviceMe, omniDir, resolveOrigin, sessionEndedMessage } from "./auth";
|
|
22
22
|
import { sharedRefresher } from "./refresh";
|
|
23
|
-
import {
|
|
23
|
+
import { formatSyncLine, formatStatusLines } from "./status-lib";
|
|
24
24
|
import { formatModeStatus } from "./yolo-lib";
|
|
25
25
|
import {
|
|
26
26
|
formatDeviceLines,
|
|
@@ -129,7 +129,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
pi.registerCommand("status", {
|
|
132
|
-
description: "Show the Omnirush session status (identity, model, grant,
|
|
132
|
+
description: "Show the Omnirush session status (identity, model, grant, session uploads)",
|
|
133
133
|
handler: async (_args: string, ctx: any) => {
|
|
134
134
|
const { me } = await fetchMe();
|
|
135
135
|
const usage = gatewayUsage();
|
|
@@ -140,7 +140,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
140
140
|
const ledger = await readSessionLedger(`${omniDir()}/${SESSION_LEDGER_FILE}`);
|
|
141
141
|
record = ledger.sessions[sessionId] ?? null;
|
|
142
142
|
} catch {
|
|
143
|
-
/*
|
|
143
|
+
/* session upload state is best effort */
|
|
144
144
|
}
|
|
145
145
|
const model = ctx?.model;
|
|
146
146
|
const statusLines = formatStatusLines({
|
|
@@ -151,7 +151,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
151
151
|
effort: usage?.["x-omnirush-effort"] ?? null,
|
|
152
152
|
grant: grant?.text ?? null,
|
|
153
153
|
grantDate: grant?.date ?? null,
|
|
154
|
-
|
|
154
|
+
sync: { sessionId, record },
|
|
155
155
|
origin: resolveOrigin(process.env),
|
|
156
156
|
version: CLI_VERSION,
|
|
157
157
|
yolo: formatModeStatus(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The CLI's session, in the desktop app's engine message format.
|
|
2
2
|
//
|
|
3
|
-
// The desktop
|
|
3
|
+
// The desktop app records the conversation as the engine (opencode)
|
|
4
4
|
// keeps it: one message per user prompt and per model step,
|
|
5
5
|
//
|
|
6
6
|
// {info: {id, sessionID, role, time: {created, completed}, modelID,
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
// outcome) comes from stream-timing.ts's `omnirush-timing` entries.
|
|
73
73
|
//
|
|
74
74
|
// Also here, pure and shared with the tests: the helpers the desktop's turn
|
|
75
|
-
// observer (apps/server/src
|
|
75
|
+
// observer (apps/server/src) applies to the engine's
|
|
76
76
|
// messages when a turn settles: the turn's model, the message budget of a
|
|
77
77
|
// flush and its split into `turn.messages` parts.
|
|
78
78
|
|
|
@@ -1010,9 +1010,9 @@ export function parseSessionFile(text: string): { header: PiEntry | null; entrie
|
|
|
1010
1010
|
return { header, entries };
|
|
1011
1011
|
}
|
|
1012
1012
|
|
|
1013
|
-
// --- the turn observer's helpers (
|
|
1013
|
+
// --- the turn observer's helpers (the desktop turn observer) -------------------------
|
|
1014
1014
|
|
|
1015
|
-
/** Latest model selection of a turn, as the
|
|
1015
|
+
/** Latest model selection of a turn, as the session uploader's session.model event records it. */
|
|
1016
1016
|
export type TurnModel = { provider_id: string | null; model_id: string | null; variant: string | null; agent: string | null };
|
|
1017
1017
|
|
|
1018
1018
|
function optionalText(value: unknown): string | null {
|
|
@@ -1023,7 +1023,7 @@ function optionalText(value: unknown): string | null {
|
|
|
1023
1023
|
* The model the turn ran on, read from the turn's first assistant message
|
|
1024
1024
|
* (providerID / modelID / mode or agent); the user message that opened the
|
|
1025
1025
|
* turn supplies the variant and agent when the assistant record lacks them.
|
|
1026
|
-
* (
|
|
1026
|
+
* (the desktop turn observer `turnModelFromMessages`.)
|
|
1027
1027
|
*/
|
|
1028
1028
|
export function turnModelFromMessages(messages: unknown): TurnModel | null {
|
|
1029
1029
|
if (!Array.isArray(messages)) return null;
|
|
@@ -1071,7 +1071,7 @@ export type TurnHistory = {
|
|
|
1071
1071
|
* The part of a session's messages a settled turn records: every message
|
|
1072
1072
|
* after the checkpoint message (all of them without one, or when the
|
|
1073
1073
|
* checkpoint is not on the branch), newest first up to `budget` bytes, and
|
|
1074
|
-
* once the budget is spent only the user's own prompts. (
|
|
1074
|
+
* once the budget is spent only the user's own prompts. (the desktop turn observer
|
|
1075
1075
|
* `readEngineHistory`, over messages already in memory.)
|
|
1076
1076
|
*/
|
|
1077
1077
|
export function turnHistory(messages: readonly EngineMessage[], checkpoint: string | undefined, budget = MAX_BACKLOG_MESSAGE_BYTES): TurnHistory {
|
|
@@ -1112,7 +1112,7 @@ export type MessagePart = { messages: unknown[]; bytes: number };
|
|
|
1112
1112
|
* A settled turn's messages (oldest first) cut into the parts its trace
|
|
1113
1113
|
* flushes carry, oldest part first: the newest messages up to `maxBytes` make
|
|
1114
1114
|
* the last part, and each earlier part takes the next older ones. One part,
|
|
1115
|
-
* empty, when there are no messages. (
|
|
1115
|
+
* empty, when there are no messages. (the desktop turn observer `messageParts`.)
|
|
1116
1116
|
*/
|
|
1117
1117
|
export function messageParts(messages: readonly unknown[], sizes: readonly number[], maxBytes = MAX_TURN_MESSAGE_BYTES): MessagePart[] {
|
|
1118
1118
|
const parts: MessagePart[] = [];
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// A sub-agent (a headless spawn_agents child: OMNIRUSH_PARENT_SESSION is
|
|
10
10
|
// set) does not load what only the interactive top agent uses — the slash
|
|
11
|
-
// commands (commands), the session capture (
|
|
11
|
+
// commands (commands), the session capture (session uploader: the parent records
|
|
12
12
|
// its children) and voice input (TUI only) — so each of the many child
|
|
13
13
|
// processes a fan-out starts stays smaller. They are imported on demand,
|
|
14
14
|
// in the same order as before, so the top agent registers exactly what it
|
|
@@ -39,11 +39,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
39
39
|
if (!subagent) (await import("./commands")).default(pi as any);
|
|
40
40
|
recorder(pi as any);
|
|
41
41
|
// Every process times its own runs (the 2.x trace fields); before the
|
|
42
|
-
//
|
|
42
|
+
// session uploader, so a settled run's idle entry is written before it is read.
|
|
43
43
|
recordStreamTiming(pi as any);
|
|
44
44
|
sota(pi as any);
|
|
45
|
-
if (!subagent) (await import("./
|
|
46
|
-
// Children still mark their own session as a sub-agent's (the
|
|
45
|
+
if (!subagent) (await import("./session-sync")).default(pi as any);
|
|
46
|
+
// Children still mark their own session as a sub-agent's (the session uploader
|
|
47
47
|
// does that for the top agent's sessions; children do not load it).
|
|
48
48
|
else (await import("./subagent-marker")).default(pi as any);
|
|
49
49
|
mcp(pi as any);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The model maintains an ordered task list (items with pending /
|
|
4
4
|
// in-progress / done statuses) while it works. Every update:
|
|
5
|
-
// - persists to <cwd>/__agent__/plan.json (the workspace
|
|
5
|
+
// - persists to <cwd>/__agent__/plan.json (the workspace uploader
|
|
6
6
|
// ships it with the workspace snapshots),
|
|
7
7
|
// - appends an `omnirush.plan` custom entry to the session JSONL (the
|
|
8
8
|
// tool call itself, with the whole list, reaches the trace as a tool
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
const PLAN_TOOL_NAME = "plan";
|
|
42
42
|
const PLAN_ENTRY_TYPE = "omnirush.plan";
|
|
43
43
|
const PLAN_STATUS_KEY = "omnirush-plan";
|
|
44
|
-
// The plan file's folder in the workspace (the CLI's earlier
|
|
44
|
+
// The plan file's folder in the workspace (the CLI's earlier uploader
|
|
45
45
|
// kept its journal there too).
|
|
46
46
|
const AGENT_DIR = "__agent__";
|
|
47
47
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Process-wide single-flight refresher shared by the sota guard and the
|
|
2
|
-
//
|
|
2
|
+
// session uploader (both run inside the pi process). Lazily created from the
|
|
3
3
|
// environment; refreshes rotate both tokens back to the state dir (0600).
|
|
4
4
|
|
|
5
5
|
import { createRefresher, omniDir, resolveOrigin } from "./auth";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Omnirush retry layer — shared by every Omnirush API call path (model
|
|
2
|
-
// stream via the sota tap,
|
|
2
|
+
// stream via the sota tap, session uploads, /device and /device/me).
|
|
3
3
|
//
|
|
4
4
|
// Goal: users ride out a backend deploy or a rate-limit window without
|
|
5
5
|
// seeing a single raw error. Retryable = 429 / 5xx / network failures.
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// the same format, as the desktop app.
|
|
3
3
|
//
|
|
4
4
|
// The capture itself is the desktop's, ported unchanged but for a few
|
|
5
|
-
// marked seams (capture/: workspace-
|
|
5
|
+
// marked seams (capture/: workspace-sync.ts, turn-diff.ts and the
|
|
6
6
|
// project archive in session-archive/, from omnirush-gui apps/server/src).
|
|
7
7
|
// This file is the CLI's counterpart of the desktop's turn observer
|
|
8
|
-
// (
|
|
8
|
+
// (the desktop turn observer) and server hooks: it tells the session uploader and the
|
|
9
9
|
// project archive what the agent does, from pi's events.
|
|
10
10
|
//
|
|
11
11
|
// a prompt goes out engine.request trace event (prompt, model,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// events), the "prompt" change snapshot, the
|
|
14
14
|
// project archive's session start (its base)
|
|
15
15
|
// a run settles the conversation since the last flush as
|
|
16
|
-
// engine messages (
|
|
16
|
+
// engine messages (engine-messages.ts): session.model,
|
|
17
17
|
// turn.messages parts when it is long, each
|
|
18
18
|
// sub-agent's new messages as session.child, the
|
|
19
19
|
// "turn_completed" snapshot with its turn.diff, and
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
// it. A prompt an extension sends (pi's input event, source "extension")
|
|
42
42
|
// is synthetic too.
|
|
43
43
|
//
|
|
44
|
-
// Silent no-op without device credentials:
|
|
44
|
+
// Silent no-op without device credentials: session uploads only run for
|
|
45
45
|
// signed-in omnirush identities.
|
|
46
46
|
|
|
47
47
|
import { createHash } from "node:crypto";
|
|
@@ -53,7 +53,7 @@ import path from "node:path";
|
|
|
53
53
|
import { createInterface } from "node:readline";
|
|
54
54
|
|
|
55
55
|
import { gatewayUrlForOrigin, omniDir, resolveOrigin } from "./auth";
|
|
56
|
-
import {
|
|
56
|
+
import { WorkspaceSync } from "./capture/workspace-sync";
|
|
57
57
|
import { SessionArchiver } from "./capture/session-archive/index";
|
|
58
58
|
import { ProjectArchiveLifecycle, projectArchiveEnabled } from "./capture/session-archive/lifecycle";
|
|
59
59
|
import {
|
|
@@ -68,14 +68,14 @@ import {
|
|
|
68
68
|
userText,
|
|
69
69
|
type EngineMessage,
|
|
70
70
|
type PiEntry,
|
|
71
|
-
} from "./
|
|
71
|
+
} from "./engine-messages";
|
|
72
72
|
import { sharedRefresher } from "./refresh";
|
|
73
73
|
import { AGENTS_HOOK, SUBAGENT_SESSIONS_DIR, type AgentsHook } from "./agents-lib";
|
|
74
74
|
import { BGSHELL_HOOK, type BgShellHook } from "./bgshell-lib";
|
|
75
75
|
|
|
76
76
|
/** info.agent of the root session's messages (a sub-agent's is its role). */
|
|
77
77
|
export const ROOT_AGENT = "omnirush-cli";
|
|
78
|
-
/** Sub-agent layers captured below the root session (the desktop's
|
|
78
|
+
/** Sub-agent layers captured below the root session (the desktop's MAX_SYNC_CHILD_SESSION_DEPTH). */
|
|
79
79
|
const MAX_CHILD_DEPTH = 3;
|
|
80
80
|
/** Sub-agent sessions followed per session (the desktop's MAX_CHILD_SESSIONS). */
|
|
81
81
|
const MAX_CHILDREN = 200;
|
|
@@ -105,7 +105,7 @@ function piAgentDir(): string {
|
|
|
105
105
|
return path.join(stateDir ? path.resolve(stateDir) : path.join(os.homedir(), ".omnirush"), "agent");
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
/** pi session ids are UUIDs; the
|
|
108
|
+
/** pi session ids are UUIDs; the session uploader refuses anything outside its id alphabet. */
|
|
109
109
|
function sanitizeSessionId(raw: string): string {
|
|
110
110
|
const id = String(raw || "");
|
|
111
111
|
return /^[A-Za-z0-9._:-]{8,128}$/.test(id) ? id : "";
|
|
@@ -128,7 +128,7 @@ function cliVersion(): string {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/** The previous
|
|
131
|
+
/** The previous session uploader's per-session change journals (`journal-<session>.ndjson`); the ported session uploader keeps none. */
|
|
132
132
|
function removeLegacyJournals(stateDir: string): void {
|
|
133
133
|
try {
|
|
134
134
|
for (const name of readdirSync(stateDir)) {
|
|
@@ -307,20 +307,20 @@ export async function parseSessionFileStream(file: string): Promise<{ header: Pi
|
|
|
307
307
|
return { header, entries };
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
type
|
|
311
|
-
|
|
310
|
+
type Sync = Pick<
|
|
311
|
+
WorkspaceSync,
|
|
312
312
|
"recordTrace" | "recordChildSession" | "childCheckpoints" | "childSessionIds"
|
|
313
313
|
>;
|
|
314
314
|
|
|
315
315
|
/**
|
|
316
316
|
* Records the sub-agent tree below a settled session: one session.child event
|
|
317
317
|
* per sub-agent carrying only its messages since its checkpoint, recursively
|
|
318
|
-
* for the sub-agents it started (
|
|
318
|
+
* for the sub-agents it started (the desktop turn observer
|
|
319
319
|
* `captureChildSessions`). `beforeRecord` hears the size of each event's
|
|
320
320
|
* messages before it is recorded.
|
|
321
321
|
*/
|
|
322
322
|
export async function captureChildSessions(input: {
|
|
323
|
-
|
|
323
|
+
sync: Sync;
|
|
324
324
|
rootSessionId: string;
|
|
325
325
|
parentSessionId: string;
|
|
326
326
|
parentEntries: readonly PiEntry[];
|
|
@@ -354,13 +354,13 @@ export async function captureChildSessions(input: {
|
|
|
354
354
|
const messages = engineMessagesFromEntries(entries, { sessionId: child.id, agent, cwd });
|
|
355
355
|
const history = turnHistory(messages, input.checkpoints[child.id], MAX_TURN_MESSAGE_BYTES);
|
|
356
356
|
if (history.omitted > 0) {
|
|
357
|
-
input.
|
|
357
|
+
input.sync.recordTrace(input.rootSessionId, "session.messages_omitted", { child_session_id: child.id, count: history.omitted });
|
|
358
358
|
}
|
|
359
359
|
const fallbackKey = `${input.rootSessionId}:${child.id}`;
|
|
360
360
|
if (child.modelFallback && !recordedFallbacks.has(fallbackKey)) {
|
|
361
361
|
// As the desktop: the sub-agent ran on the main model, and why.
|
|
362
362
|
recordedFallbacks.add(fallbackKey);
|
|
363
|
-
input.
|
|
363
|
+
input.sync.recordTrace(input.rootSessionId, "subagent.model_fallback", {
|
|
364
364
|
child_session_id: child.id,
|
|
365
365
|
requested: child.modelFallback.requested,
|
|
366
366
|
used: child.modelFallback.used,
|
|
@@ -371,7 +371,7 @@ export async function captureChildSessions(input: {
|
|
|
371
371
|
}
|
|
372
372
|
if (history.delta.length > 0 || !input.known.has(child.id)) {
|
|
373
373
|
await input.beforeRecord(history.sizes.reduce((total, bytes) => total + bytes, 0));
|
|
374
|
-
input.
|
|
374
|
+
input.sync.recordChildSession(input.rootSessionId, {
|
|
375
375
|
childSessionId: child.id,
|
|
376
376
|
parentSessionId: input.parentSessionId,
|
|
377
377
|
depth: input.depth,
|
|
@@ -391,7 +391,7 @@ export default function (pi: any) {
|
|
|
391
391
|
const parentSession = (process.env.OMNIRUSH_PARENT_SESSION || "").trim();
|
|
392
392
|
if (parentSession) {
|
|
393
393
|
// Its session says so (subagent-marker.ts; index.ts loads it for
|
|
394
|
-
// sub-agent processes, which do not load the
|
|
394
|
+
// sub-agent processes, which do not load the session uploader).
|
|
395
395
|
markSubagentSession(pi, parentSession);
|
|
396
396
|
return;
|
|
397
397
|
}
|
|
@@ -411,7 +411,7 @@ export default function (pi: any) {
|
|
|
411
411
|
if (level !== "warn" && !verbose) return;
|
|
412
412
|
try {
|
|
413
413
|
const detail = attributes && Object.keys(attributes).length > 0 ? ` ${JSON.stringify(attributes)}` : "";
|
|
414
|
-
process.stderr.write(`omnirush
|
|
414
|
+
process.stderr.write(`omnirush session upload: ${message}${detail}\n`);
|
|
415
415
|
} catch {
|
|
416
416
|
/* stderr may be closed late in shutdown */
|
|
417
417
|
}
|
|
@@ -431,7 +431,7 @@ export default function (pi: any) {
|
|
|
431
431
|
|
|
432
432
|
// Uploads carry the CURRENT on-disk token, not the one this process
|
|
433
433
|
// last saw: another terminal or a sub-agent may have rotated the pair.
|
|
434
|
-
// Only requests that already carry a bearer (
|
|
434
|
+
// Only requests that already carry a bearer (session upload, archive API) are
|
|
435
435
|
// touched — presigned S3 part PUTs pass through unchanged.
|
|
436
436
|
const withCurrentBearer = (input: string, init?: RequestInit): Promise<Response> => {
|
|
437
437
|
const headers = new Headers(init?.headers ?? {});
|
|
@@ -450,7 +450,7 @@ export default function (pi: any) {
|
|
|
450
450
|
return globalThis.fetch(input, init);
|
|
451
451
|
};
|
|
452
452
|
|
|
453
|
-
const
|
|
453
|
+
const sync = new WorkspaceSync({
|
|
454
454
|
gatewayUrl,
|
|
455
455
|
accessToken,
|
|
456
456
|
refreshAccessToken,
|
|
@@ -484,12 +484,12 @@ export default function (pi: any) {
|
|
|
484
484
|
});
|
|
485
485
|
const archive = new ProjectArchiveLifecycle({
|
|
486
486
|
archiver,
|
|
487
|
-
enabled: projectArchiveEnabled(process.env) &&
|
|
487
|
+
enabled: projectArchiveEnabled(process.env) && sync.enabled,
|
|
488
488
|
log,
|
|
489
489
|
baseIdleMs: ARCHIVE_BASE_IDLE_MS,
|
|
490
490
|
baseMaxDeferMs: ARCHIVE_BASE_MAX_DEFER_MS,
|
|
491
491
|
});
|
|
492
|
-
if (!
|
|
492
|
+
if (!sync.enabled) {
|
|
493
493
|
// Signed out: clear what a previous run left queued (no network).
|
|
494
494
|
archive.start();
|
|
495
495
|
return;
|
|
@@ -502,21 +502,21 @@ export default function (pi: any) {
|
|
|
502
502
|
id: string;
|
|
503
503
|
root: string;
|
|
504
504
|
ctx: any;
|
|
505
|
-
/** A prompt went out in this process: the
|
|
505
|
+
/** A prompt went out in this process: the session uploader follows the session. */
|
|
506
506
|
started: boolean;
|
|
507
507
|
/** A prompt went out and its run has not settled. */
|
|
508
508
|
running: boolean;
|
|
509
|
-
/** The id of the last engine message recorded (the
|
|
509
|
+
/** The id of the last engine message recorded (the session uploader's checkpoint). */
|
|
510
510
|
lastMessageId: string | undefined;
|
|
511
511
|
toolStarts: Map<string, number>;
|
|
512
512
|
seenFiles: Map<string, string>;
|
|
513
513
|
/** Prompts sent in this process with the model and effort they went with, until their user entry is found. */
|
|
514
514
|
pendingPrompts: Array<{ at: number; model: { providerID: string; modelID: string } | null; variant: string | null }>;
|
|
515
|
-
/** User entry id -> what its prompt was sent with (
|
|
515
|
+
/** User entry id -> what its prompt was sent with (engine-messages promptSettings). */
|
|
516
516
|
promptSettings: Map<string, { model: { providerID: string; modelID: string } | null; variant: string | null }>;
|
|
517
517
|
/** Prompts an extension sent (pi's input event, source "extension"), until their user entry is found. */
|
|
518
518
|
extensionInputs: Array<{ at: number; text: string }>;
|
|
519
|
-
/** User entries the user did not submit -> their parts' metadata (
|
|
519
|
+
/** User entries the user did not submit -> their parts' metadata (engine-messages injectedPrompts). */
|
|
520
520
|
injectedPrompts: Map<string, Record<string, unknown>>;
|
|
521
521
|
/** Whether the session is a sub-agent's (its first prompt the parent agent's task), once looked up. */
|
|
522
522
|
origin: Promise<void> | null;
|
|
@@ -581,7 +581,7 @@ export default function (pi: any) {
|
|
|
581
581
|
};
|
|
582
582
|
|
|
583
583
|
/**
|
|
584
|
-
* The session `ctx` is on. Its
|
|
584
|
+
* The session `ctx` is on. Its upload starts with its first prompt in
|
|
585
585
|
* this process, as the desktop starts it on a prompt dispatch: opening a
|
|
586
586
|
* session and leaving without a prompt uploads nothing.
|
|
587
587
|
*/
|
|
@@ -609,7 +609,7 @@ export default function (pi: any) {
|
|
|
609
609
|
session.ctx = ctx;
|
|
610
610
|
// Idempotent: a session already started in this process keeps its segment.
|
|
611
611
|
if (prompted) {
|
|
612
|
-
|
|
612
|
+
sync.startSession(id, workspaceIdFor(session.root), session.root);
|
|
613
613
|
session.started = true;
|
|
614
614
|
}
|
|
615
615
|
return session;
|
|
@@ -624,20 +624,20 @@ export default function (pi: any) {
|
|
|
624
624
|
const { id } = state;
|
|
625
625
|
try {
|
|
626
626
|
await captureChildSessions({
|
|
627
|
-
|
|
627
|
+
sync,
|
|
628
628
|
rootSessionId: id,
|
|
629
629
|
parentSessionId: id,
|
|
630
630
|
parentEntries: state.ctx?.sessionManager?.getBranch?.() ?? [],
|
|
631
631
|
depth: 1,
|
|
632
632
|
sessionDirs: sessionDirs(state.ctx),
|
|
633
|
-
checkpoints: await
|
|
634
|
-
known: new Set(await
|
|
633
|
+
checkpoints: await sync.childCheckpoints(id),
|
|
634
|
+
known: new Set(await sync.childSessionIds(id)),
|
|
635
635
|
visited: new Set([id]),
|
|
636
636
|
seenFiles: state.seenFiles,
|
|
637
637
|
beforeRecord: room,
|
|
638
638
|
});
|
|
639
639
|
} catch (error) {
|
|
640
|
-
|
|
640
|
+
sync.recordTrace(id, "session.children_failed", { error: error instanceof Error ? error.message : "unknown" });
|
|
641
641
|
}
|
|
642
642
|
};
|
|
643
643
|
|
|
@@ -653,9 +653,9 @@ export default function (pi: any) {
|
|
|
653
653
|
let pending = 0;
|
|
654
654
|
return async (bytes: number): Promise<void> => {
|
|
655
655
|
if (pending > 0 && pending + bytes > MAX_TURN_MESSAGE_BYTES) {
|
|
656
|
-
|
|
656
|
+
sync.flushTrace(id);
|
|
657
657
|
pending = 0;
|
|
658
|
-
await
|
|
658
|
+
await sync.drained(id);
|
|
659
659
|
}
|
|
660
660
|
pending += bytes;
|
|
661
661
|
};
|
|
@@ -673,37 +673,37 @@ export default function (pi: any) {
|
|
|
673
673
|
let parts: Array<{ messages: unknown[]; bytes: number }> = [{ messages: [], bytes: 0 }];
|
|
674
674
|
try {
|
|
675
675
|
messages = messagesOf(state);
|
|
676
|
-
const checkpoint = state.lastMessageId ?? (await
|
|
676
|
+
const checkpoint = state.lastMessageId ?? (await sync.sessionCheckpoint(id)).lastMessageId;
|
|
677
677
|
const history = turnHistory(messages, checkpoint);
|
|
678
678
|
const lastId = messages.at(-1)?.info.id;
|
|
679
679
|
if (typeof lastId === "string") {
|
|
680
680
|
state.lastMessageId = lastId;
|
|
681
|
-
void
|
|
681
|
+
void sync.setSessionCheckpoint(id, lastId);
|
|
682
682
|
}
|
|
683
|
-
if (history.omitted > 0)
|
|
683
|
+
if (history.omitted > 0) sync.recordTrace(id, "session.messages_omitted", { count: history.omitted });
|
|
684
684
|
const model = turnModelFromMessages(history.delta);
|
|
685
|
-
if (model)
|
|
685
|
+
if (model) sync.recordSessionModel(id, model);
|
|
686
686
|
// More messages than one flush holds: the older ones go ahead, a flush per part.
|
|
687
687
|
parts = messageParts(history.delta, history.sizes);
|
|
688
688
|
for (const [index, part] of parts.slice(0, -1).entries()) {
|
|
689
|
-
|
|
690
|
-
|
|
689
|
+
sync.recordTrace(id, "turn.messages", { messages: part.messages, part: index + 1, parts: parts.length });
|
|
690
|
+
sync.flushTrace(id);
|
|
691
691
|
// One part waits in memory at a time (a resumed session's backlog is up to 32 MiB).
|
|
692
|
-
await
|
|
692
|
+
await sync.drained(id);
|
|
693
693
|
}
|
|
694
694
|
} catch (error) {
|
|
695
|
-
|
|
695
|
+
sync.recordTrace(id, "session.messages_failed", { error: error instanceof Error ? error.message : "unknown" });
|
|
696
696
|
}
|
|
697
697
|
const newest = parts.at(-1)!;
|
|
698
698
|
// Sub-agent messages flush on their own before they would crowd the turn's out.
|
|
699
699
|
const room = childRoom(id);
|
|
700
700
|
await captureChildren(state, room);
|
|
701
701
|
await room(newest.bytes);
|
|
702
|
-
|
|
702
|
+
sync.recordTrace(id, "session.idle", { status });
|
|
703
703
|
// The turn snapshot runs first so the artifacts it discovers are part of
|
|
704
704
|
// the trace flushed right behind it.
|
|
705
|
-
|
|
706
|
-
|
|
705
|
+
sync.captureSnapshot(id, "turn_completed");
|
|
706
|
+
sync.flushTrace(id, { messages: newest.messages });
|
|
707
707
|
archive.turnCompleted(id, messages);
|
|
708
708
|
};
|
|
709
709
|
|
|
@@ -735,14 +735,14 @@ export default function (pi: any) {
|
|
|
735
735
|
const state = session;
|
|
736
736
|
if (!state?.started) return;
|
|
737
737
|
for (const child of interrupted) {
|
|
738
|
-
|
|
738
|
+
sync.recordTrace(state.id, "session.child_interrupted", { child_session_id: child.sessionId, agent: child.role });
|
|
739
739
|
}
|
|
740
740
|
const lastId = messagesOf(state).at(-1)?.info.id;
|
|
741
741
|
if (typeof lastId !== "string" || lastId === state.lastMessageId) {
|
|
742
742
|
// No new message of its own: what its sub-agents did since the last settle.
|
|
743
743
|
settling = settling.then(async () => {
|
|
744
744
|
await captureChildren(state, childRoom(state.id));
|
|
745
|
-
|
|
745
|
+
sync.flushTrace(state.id);
|
|
746
746
|
}).catch(() => undefined);
|
|
747
747
|
await settling;
|
|
748
748
|
return;
|
|
@@ -755,7 +755,7 @@ export default function (pi: any) {
|
|
|
755
755
|
let stopping: Promise<void> | null = null;
|
|
756
756
|
const shutdown = (budgetMs: number): Promise<void> => (stopping ??= stop(budgetMs));
|
|
757
757
|
const stop = async (budgetMs: number): Promise<void> => {
|
|
758
|
-
// A ref'd timer: the
|
|
758
|
+
// A ref'd timer: the session uploader's and the archive's own timers are unref'd,
|
|
759
759
|
// and pi awaits this handler without holding the process open, so without
|
|
760
760
|
// it a process with nothing else to do exits in the middle of an upload.
|
|
761
761
|
let budget: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -770,12 +770,12 @@ export default function (pi: any) {
|
|
|
770
770
|
if (state) {
|
|
771
771
|
// The settled turn's own snapshot (its turn.diff) and trace go out
|
|
772
772
|
// before the session ends: a finished session skips queued captures.
|
|
773
|
-
await
|
|
774
|
-
|
|
773
|
+
await sync.idle(state.id);
|
|
774
|
+
sync.finishSession(state.id);
|
|
775
775
|
}
|
|
776
776
|
// The base and turn deltas upload now; what is left waits in the queue.
|
|
777
777
|
await archive.settled();
|
|
778
|
-
await
|
|
778
|
+
await sync.stop();
|
|
779
779
|
})(),
|
|
780
780
|
deadline,
|
|
781
781
|
]);
|
|
@@ -837,7 +837,7 @@ export default function (pi: any) {
|
|
|
837
837
|
// The prompt as the desktop records its engine request: text, attached
|
|
838
838
|
// files (inline bytes replaced by a marker), model, variant, agent and
|
|
839
839
|
// the system prompt it ran with.
|
|
840
|
-
|
|
840
|
+
sync.recordTrace(state.id, "engine.request", {
|
|
841
841
|
method: "POST",
|
|
842
842
|
path: `/session/${state.id}/prompt`,
|
|
843
843
|
body: {
|
|
@@ -861,7 +861,7 @@ export default function (pi: any) {
|
|
|
861
861
|
images.forEach((image: any, index: number) => {
|
|
862
862
|
if (typeof image?.data !== "string") return;
|
|
863
863
|
const bytes = Buffer.from(image.data, "base64");
|
|
864
|
-
|
|
864
|
+
sync.recordAttachment(state.id, {
|
|
865
865
|
name: `image-${index + 1}`,
|
|
866
866
|
mime: typeof image.mimeType === "string" ? image.mimeType : "application/octet-stream",
|
|
867
867
|
bytes: bytes.length,
|
|
@@ -869,7 +869,7 @@ export default function (pi: any) {
|
|
|
869
869
|
text: null,
|
|
870
870
|
});
|
|
871
871
|
});
|
|
872
|
-
|
|
872
|
+
sync.captureSnapshot(state.id, "prompt");
|
|
873
873
|
archive.sessionStarted({
|
|
874
874
|
sessionId: state.id,
|
|
875
875
|
root: state.root,
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// no imports beyond node builtins, so node:test covers them directly (the
|
|
3
3
|
// command handler in commands.ts gathers the live inputs).
|
|
4
4
|
|
|
5
|
-
export interface
|
|
5
|
+
export interface SyncStatusInput {
|
|
6
6
|
/** pi session id (empty when unknown). */
|
|
7
7
|
sessionId: string;
|
|
8
|
-
/** Ledger record for the session, when the
|
|
8
|
+
/** Ledger record for the session, when the session uploader has one. */
|
|
9
9
|
record?: {
|
|
10
10
|
segment: number;
|
|
11
11
|
nextSequence: number;
|
|
@@ -14,19 +14,19 @@ export interface CollectorStatusInput {
|
|
|
14
14
|
} | null;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/** The
|
|
18
|
-
export function
|
|
19
|
-
if (!sessionId) return "
|
|
17
|
+
/** The session uploader line: current session, parts uploaded, last upload time. */
|
|
18
|
+
export function formatSyncLine({ sessionId, record }: SyncStatusInput): string {
|
|
19
|
+
if (!sessionId) return "session uploads: no session yet";
|
|
20
20
|
const short = sessionId.slice(0, 8);
|
|
21
21
|
if (!record) {
|
|
22
|
-
return `
|
|
22
|
+
return `session uploads: session ${short} — no uploads yet`;
|
|
23
23
|
}
|
|
24
24
|
const parts = `session ${short} — ${record.nextSequence} part${record.nextSequence === 1 ? "" : "s"} uploaded`;
|
|
25
25
|
const bytes = typeof record.sentBytes === "number" && record.sentBytes > 0
|
|
26
26
|
? ` · ${(record.sentBytes / 1024).toFixed(0)} kB sent`
|
|
27
27
|
: "";
|
|
28
28
|
const seen = record.lastSeenAt ? ` · last upload ${record.lastSeenAt}` : "";
|
|
29
|
-
return `
|
|
29
|
+
return `session uploads: ${parts}${bytes}${seen} (segment ${record.segment})`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** Truncate an identity string for one-line display. */
|
|
@@ -47,7 +47,7 @@ export interface StatusInput {
|
|
|
47
47
|
/** Human grant summary, e.g. "997.2M of 1.0B tokens remaining". */
|
|
48
48
|
grant?: string | null;
|
|
49
49
|
grantDate?: string | null;
|
|
50
|
-
|
|
50
|
+
sync: SyncStatusInput;
|
|
51
51
|
/** Manager origin the CLI talks to. */
|
|
52
52
|
origin: string;
|
|
53
53
|
/** omnirush CLI version ("unknown" when not provided by the launcher). */
|
|
@@ -80,7 +80,7 @@ export function formatStatusLines(input: StatusInput): string[] {
|
|
|
80
80
|
lines.push("grant: no data yet (appears after the first request or via /usage)");
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
lines.push(
|
|
83
|
+
lines.push(formatSyncLine(input.sync));
|
|
84
84
|
if (input.yolo) lines.push(input.yolo);
|
|
85
85
|
lines.push(`manager: ${input.origin}`);
|
|
86
86
|
lines.push(`version: ${input.version}`);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// `omnirush-timing` entries (custom entries: never part of the model's
|
|
9
9
|
// context), so every process records its own, a sub-agent's included, and
|
|
10
10
|
// a session read back later (a resumed one, a sub-agent's file) still has
|
|
11
|
-
// them.
|
|
11
|
+
// them. engine-messages.ts reads them into the trace's 2.x fields:
|
|
12
12
|
//
|
|
13
13
|
// one entry per model step, at the end of its turn
|
|
14
14
|
// {step: <the assistant message's timestamp>, streamed, blocks:
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
// one entry when the run settles
|
|
17
17
|
// {idle: {at, outcome: "succeeded" | "failed" | "interrupted"}}
|
|
18
18
|
//
|
|
19
|
-
// Loaded before the
|
|
20
|
-
// when the
|
|
19
|
+
// Loaded before the session uploader (index.ts): the idle entry is on the branch
|
|
20
|
+
// when the session uploader reads the settled run.
|
|
21
21
|
|
|
22
|
-
import { TIMING_ENTRY_TYPE } from "./
|
|
22
|
+
import { TIMING_ENTRY_TYPE } from "./engine-messages";
|
|
23
23
|
|
|
24
24
|
type Block = { index: number; created?: number; completed?: number };
|
|
25
25
|
type Step = { streamed?: number; blocks: Map<number, Block>; message?: any };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// A sub-agent process (spawn_agents sets OMNIRUSH_PARENT_SESSION) marks its
|
|
2
2
|
// own session with an `omnirush-subagent` entry naming its parent, before
|
|
3
3
|
// the task: should that session ever be continued as a main session, its
|
|
4
|
-
// first prompt is known to be the parent agent's (
|
|
5
|
-
// as synthetic). Kept out of
|
|
6
|
-
// not load the
|
|
4
|
+
// first prompt is known to be the parent agent's (session-sync.ts records it
|
|
5
|
+
// as synthetic). Kept out of session-sync.ts because sub-agent processes do
|
|
6
|
+
// not load the session uploader (index.ts keeps children light).
|
|
7
7
|
|
|
8
|
-
import { SUBAGENT_ENTRY_TYPE } from "./
|
|
8
|
+
import { SUBAGENT_ENTRY_TYPE } from "./engine-messages";
|
|
9
9
|
|
|
10
10
|
export default function markSubagentSession(pi: any, parentSession: string = (process.env.OMNIRUSH_PARENT_SESSION || "").trim()): void {
|
|
11
11
|
if (!parentSession) return;
|
|
@@ -34,7 +34,7 @@ export const SWARM_LAYER_ENV = "OMNIRUSH_SWARM_LAYER";
|
|
|
34
34
|
export const SWARM_SKILL_DESCRIPTION = `Required before starting ${SWARM_MIN_AGENTS} or more sub-agents for one request (a swarm, or the user asks for several agents): the shared-board procedure. Never for 1-2 sub-agents or a small request.`;
|
|
35
35
|
|
|
36
36
|
/** The always-on line (a spawn_agents guideline). */
|
|
37
|
-
export const SWARM_GUIDELINE = `Before you start ${SWARM_MIN_AGENTS} or more sub-agents for one request (large work that splits into ${SWARM_MIN_AGENTS}+ independent parts, or the user asks for a swarm or for several agents), load the ${SWARM_SKILL_NAME} skill first and follow it. Otherwise answer directly,
|
|
37
|
+
export const SWARM_GUIDELINE = `Before you start ${SWARM_MIN_AGENTS} or more sub-agents for one request (large work that splits into ${SWARM_MIN_AGENTS}+ independent parts, or the user asks for a swarm or for several agents), load the ${SWARM_SKILL_NAME} skill first and follow it. Otherwise do not load it: answer simple requests directly, and when sub-agents are warranted use at most 1-2 sub-agents with spawn_agents and no board or coordination file.`;
|
|
38
38
|
|
|
39
39
|
export const SWARM_SKILL = `# Sub-agent swarm
|
|
40
40
|
|
|
@@ -81,7 +81,7 @@ export function swarmSubagentNote(layer: number): string {
|
|
|
81
81
|
`You are a sub-agent (layer ${layer}) in a swarm coordinated through \`${SWARM_FILE}\` in the workspace.`,
|
|
82
82
|
`Read \`${SWARM_FILE}\` before you start. Find your task id in your task (or the row that matches your task), set its status to running, and when you finish set it to done with a one-line result and append your findings under your task id. Edit only your own rows and sections, and re-read the file right before each edit because other agents write to it too. Do not move or archive the board: the main agent does that.`,
|
|
83
83
|
layer < 3
|
|
84
|
-
? "
|
|
84
|
+
? "Do your task yourself. Start sub-agents only if your task explicitly tells you to: then add sub-task rows (for example T1.1, T1.2) first and give each sub-agent the same instructions."
|
|
85
85
|
: "Do not delegate further: do the work yourself.",
|
|
86
86
|
].join("\n");
|
|
87
87
|
}
|