@omercnet/paseo-omp 5.1.1 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/OVERVIEW.md +1 -0
- package/README.md +2 -2
- package/client/provider-image.tsx +127 -14
- package/client/provider-launch-settings.tsx +46 -2
- package/client/subagents-state.ts +108 -0
- package/client/subagents-strip.tsx +391 -0
- package/index.client.tsx +9 -0
- package/index.server.ts +25 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +5 -8
- package/scripts/prepare-dependencies.mjs +7 -10
- package/server/mcp-browser.ts +3 -1
- package/server/package-version.ts +1 -1
- package/server/provider/connection.ts +43 -8
- package/server/provider/omp-rpc-process.ts +9 -9
- package/server/provider/omp-rpc-protocol.ts +40 -2
- package/server/provider/omp-rpc-transport.ts +5 -2
- package/server/provider/omp-rpc.ts +25 -0
- package/server/provider/registration.ts +18 -0
- package/server/provider/session-compaction.ts +29 -25
- package/server/provider/session-descriptors.ts +36 -30
- package/server/provider/session-events.ts +1 -0
- package/server/provider/session-history.ts +2 -0
- package/server/provider/session-limit.ts +32 -0
- package/server/provider/session-terminal.ts +52 -1
- package/server/provider/session.ts +255 -67
- package/server/provider/subsessions.ts +109 -7
- package/server/provider/timeline-projector.ts +41 -26
- package/server/subagents.ts +97 -0
- package/shared/provider-launch-settings.ts +4 -0
- package/shared/subagents.ts +60 -0
|
@@ -10,12 +10,11 @@ const MAX_DESCRIPTOR_PREFIX_BYTES = 64 * 1024;
|
|
|
10
10
|
const MAX_DESCRIPTOR_SUFFIX_BYTES = 64 * 1024;
|
|
11
11
|
const MAX_PROMPT_PREVIEW_CHARS = 160;
|
|
12
12
|
const MAX_DIRECTORY_DEPTH = 8;
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
// No wall-clock cutoff: it silently drops sessions on a slow disk. OMP keeps one directory per
|
|
14
|
+
// workspace, so the count caps must exceed real session trees or lookups silently miss.
|
|
15
15
|
const MAX_SCAN_DIRECTORIES = 65_536;
|
|
16
16
|
const MAX_SCAN_FILES = 262_144;
|
|
17
17
|
const MAX_SCAN_BYTES = 16 * 1024 * 1024;
|
|
18
|
-
const MAX_SCAN_MS = 1_000;
|
|
19
18
|
const SCAN_YIELD_INTERVAL = 128;
|
|
20
19
|
const MAX_LIST_RESULTS = 500;
|
|
21
20
|
const MAX_CHILD_TRANSCRIPT_BYTES = 16 * 1024 * 1024;
|
|
@@ -59,10 +58,10 @@ export interface OmpSessionListOptions {
|
|
|
59
58
|
limit?: number;
|
|
60
59
|
sessionId?: string;
|
|
61
60
|
sessionDir?: string;
|
|
61
|
+
signal?: AbortSignal;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
interface ScanBudget {
|
|
65
|
-
startedAt: number;
|
|
66
65
|
directories: number;
|
|
67
66
|
files: number;
|
|
68
67
|
bytes: number;
|
|
@@ -155,8 +154,7 @@ function budgetExceeded(budget: ScanBudget): boolean {
|
|
|
155
154
|
budget.exhausted ||
|
|
156
155
|
budget.directories >= MAX_SCAN_DIRECTORIES ||
|
|
157
156
|
budget.files >= MAX_SCAN_FILES ||
|
|
158
|
-
budget.bytes >= MAX_SCAN_BYTES
|
|
159
|
-
Date.now() - budget.startedAt >= MAX_SCAN_MS
|
|
157
|
+
budget.bytes >= MAX_SCAN_BYTES
|
|
160
158
|
);
|
|
161
159
|
}
|
|
162
160
|
|
|
@@ -418,11 +416,13 @@ async function scanSessionFiles(
|
|
|
418
416
|
root: string,
|
|
419
417
|
budget: ScanBudget,
|
|
420
418
|
visit: (file: string) => Promise<boolean>,
|
|
419
|
+
signal?: AbortSignal,
|
|
421
420
|
): Promise<void> {
|
|
422
421
|
const pending: Array<{ path: string; depth: number }> = [{ path: root, depth: 0 }];
|
|
423
422
|
while (pending.length > 0 && !budgetExceeded(budget)) {
|
|
424
423
|
const current = pending.pop();
|
|
425
424
|
if (!current) return;
|
|
425
|
+
signal?.throwIfAborted();
|
|
426
426
|
budget.directories += 1;
|
|
427
427
|
let directory: Dir;
|
|
428
428
|
try {
|
|
@@ -432,6 +432,7 @@ async function scanSessionFiles(
|
|
|
432
432
|
}
|
|
433
433
|
try {
|
|
434
434
|
for await (const entry of directory) {
|
|
435
|
+
signal?.throwIfAborted();
|
|
435
436
|
budget.entries += 1;
|
|
436
437
|
if (budget.entries % SCAN_YIELD_INTERVAL === 0) await yieldToEventLoop();
|
|
437
438
|
if (budgetExceeded(budget)) return;
|
|
@@ -469,7 +470,6 @@ export async function listOmpSessionDescriptors(
|
|
|
469
470
|
const query = options.query?.trim().toLowerCase();
|
|
470
471
|
const matches: OmpSessionDescriptor[] = [];
|
|
471
472
|
const budget: ScanBudget = {
|
|
472
|
-
startedAt: Date.now(),
|
|
473
473
|
directories: 0,
|
|
474
474
|
files: 0,
|
|
475
475
|
bytes: 0,
|
|
@@ -481,29 +481,35 @@ export async function listOmpSessionDescriptors(
|
|
|
481
481
|
? join(environment.HOME ?? environment.USERPROFILE ?? homedir(), options.sessionDir.slice(2))
|
|
482
482
|
: resolve(options.cwd ?? homedir(), options.sessionDir)
|
|
483
483
|
: ompSessionDir(environment);
|
|
484
|
-
await scanSessionFiles(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
!
|
|
491
|
-
|
|
492
|
-
(
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
484
|
+
await scanSessionFiles(
|
|
485
|
+
root,
|
|
486
|
+
budget,
|
|
487
|
+
async (file) => {
|
|
488
|
+
const fileName = basename(file);
|
|
489
|
+
const stem = fileName.slice(0, -".jsonl".length);
|
|
490
|
+
if (requestedId && !stem.endsWith(`_${requestedId}`)) return true;
|
|
491
|
+
const descriptor = await parseDescriptor(file, budget);
|
|
492
|
+
if (
|
|
493
|
+
!descriptor ||
|
|
494
|
+
!stem.endsWith(`_${descriptor.id}`) ||
|
|
495
|
+
(options.cwd !== undefined && descriptor.cwd !== options.cwd)
|
|
496
|
+
)
|
|
497
|
+
return !budgetExceeded(budget);
|
|
498
|
+
if (
|
|
499
|
+
query &&
|
|
500
|
+
!descriptor.id.toLowerCase().includes(query) &&
|
|
501
|
+
!descriptor.title?.toLowerCase().includes(query) &&
|
|
502
|
+
!descriptor.firstPromptPreview?.toLowerCase().includes(query) &&
|
|
503
|
+
!descriptor.lastPromptPreview?.toLowerCase().includes(query)
|
|
504
|
+
) {
|
|
505
|
+
return !budgetExceeded(budget);
|
|
506
|
+
}
|
|
507
|
+
retainNewest(matches, descriptor, retentionLimit);
|
|
508
|
+
return !requestedId || matches.length < 2;
|
|
509
|
+
},
|
|
510
|
+
options.signal,
|
|
511
|
+
);
|
|
512
|
+
options.signal?.throwIfAborted();
|
|
507
513
|
return matches;
|
|
508
514
|
}
|
|
509
515
|
|
|
@@ -24,6 +24,7 @@ export function isRuntimeConfigEvent(event: OmpRpcEvent): boolean {
|
|
|
24
24
|
return (
|
|
25
25
|
event.type === "model_changed" ||
|
|
26
26
|
event.type === "thinking_level_changed" ||
|
|
27
|
+
event.type === "config_update" ||
|
|
27
28
|
event.type === "retry_fallback_applied" ||
|
|
28
29
|
event.type === "retry_fallback_succeeded"
|
|
29
30
|
);
|
|
@@ -119,6 +119,7 @@ export class OmpHistorySession implements OmpOpenedSession {
|
|
|
119
119
|
environment?: NodeJS.ProcessEnv,
|
|
120
120
|
hostInheritEnv: readonly string[] = [],
|
|
121
121
|
scheduler: OmpTimelineScheduler = defaultOmpTimelineScheduler,
|
|
122
|
+
signal?: AbortSignal,
|
|
122
123
|
): Promise<OmpHistorySession> {
|
|
123
124
|
const nativeSessionId = ompPersistenceSessionId(input);
|
|
124
125
|
if (!nativeSessionId || input.history !== "replay" || !input.config.persist) {
|
|
@@ -141,6 +142,7 @@ export class OmpHistorySession implements OmpOpenedSession {
|
|
|
141
142
|
nativeSessionId,
|
|
142
143
|
input.config.cwd,
|
|
143
144
|
normalized.sessionDir,
|
|
145
|
+
signal,
|
|
144
146
|
);
|
|
145
147
|
if (!descriptor.transcriptFile) {
|
|
146
148
|
throw new OmpPublicError("OMP session transcript could not be resolved in this workspace");
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DEFAULT_MAX_SESSIONS } from "../../shared/provider-launch-settings";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Counts open sessions across every provider connection that shares this instance. The plugin
|
|
5
|
+
* shares one instance between all of its OMP providers, so the limit applies to the whole host.
|
|
6
|
+
*/
|
|
7
|
+
export class OmpSessionLimit {
|
|
8
|
+
private readonly counts = new Set<() => number>();
|
|
9
|
+
maxSessions = DEFAULT_MAX_SESSIONS;
|
|
10
|
+
|
|
11
|
+
/** `readConfigured` returns the configured limit, or `undefined` to keep the current one. */
|
|
12
|
+
constructor(private readonly readConfigured?: () => Promise<number | undefined>) {}
|
|
13
|
+
|
|
14
|
+
async refresh(): Promise<void> {
|
|
15
|
+
const configured = await this.readConfigured?.();
|
|
16
|
+
if (configured !== undefined) this.maxSessions = configured;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Adds a connection's open sessions to the total until the returned function runs. */
|
|
20
|
+
track(count: () => number): () => void {
|
|
21
|
+
this.counts.add(count);
|
|
22
|
+
return () => {
|
|
23
|
+
this.counts.delete(count);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get openSessions(): number {
|
|
28
|
+
let total = 0;
|
|
29
|
+
for (const count of this.counts) total += count();
|
|
30
|
+
return total;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { ProviderError } from "@getpaseo/plugin/server/provider";
|
|
3
3
|
import type { OmpMessage, OmpRpcEvent } from "./omp-rpc-protocol";
|
|
4
|
+
import type { CompactionOutcome } from "./session-compaction";
|
|
4
5
|
|
|
5
6
|
export type PendingUser = {
|
|
6
7
|
clientMessageId: string;
|
|
@@ -12,7 +13,7 @@ export type PendingUser = {
|
|
|
12
13
|
type TerminalCorrelationEvidence = "fresh-native-user" | "current-assistant";
|
|
13
14
|
|
|
14
15
|
type TerminalCorrelation = {
|
|
15
|
-
policy: "initial-turn" | "ordered-legacy" | "native-command";
|
|
16
|
+
policy: "initial-turn" | "ordered-legacy" | "native-command" | "native-run";
|
|
16
17
|
evidence: Map<TerminalCorrelationEvidence, number>;
|
|
17
18
|
};
|
|
18
19
|
|
|
@@ -24,6 +25,7 @@ export type TerminalCandidate = {
|
|
|
24
25
|
| "initial-turn"
|
|
25
26
|
| "ordered-legacy"
|
|
26
27
|
| "native-command"
|
|
28
|
+
| "native-run"
|
|
27
29
|
| "interrupted"
|
|
28
30
|
| "ambiguous";
|
|
29
31
|
};
|
|
@@ -55,6 +57,7 @@ export type ActiveTurn = {
|
|
|
55
57
|
usageSampleFloor: number;
|
|
56
58
|
manualCompaction: boolean;
|
|
57
59
|
manualCompactionPending: boolean;
|
|
60
|
+
manualCompactionOutcome?: CompactionOutcome;
|
|
58
61
|
manualCompactionDeadlineTimer?: unknown;
|
|
59
62
|
agentEndPending: boolean;
|
|
60
63
|
agentEndRetryTimer?: unknown;
|
|
@@ -64,11 +67,14 @@ export type ActiveTurn = {
|
|
|
64
67
|
terminalizing: boolean;
|
|
65
68
|
terminalization?: Promise<void>;
|
|
66
69
|
terminalOutcome?: TurnOutcome;
|
|
70
|
+
/** Listeners may hold this turn's terminal state from here on, even if publishing threw. */
|
|
71
|
+
terminalEmitAttempted: boolean;
|
|
67
72
|
operationalTerminalStage?: "failed" | "unresolved";
|
|
68
73
|
terminalWake?: VoidDeferred;
|
|
69
74
|
steerReady: VoidDeferred;
|
|
70
75
|
steersInFlight: number;
|
|
71
76
|
deferredAgentEnd?: TerminalCandidate;
|
|
77
|
+
heldAgentEnd?: HeldAgentEnd;
|
|
72
78
|
activeToolCallIds: Set<string>;
|
|
73
79
|
bufferedEvents: OmpRpcEvent[];
|
|
74
80
|
pendingUsers: PendingUser[];
|
|
@@ -82,6 +88,38 @@ export type ActiveTurn = {
|
|
|
82
88
|
lastCompletedAssistantEntryId?: string;
|
|
83
89
|
};
|
|
84
90
|
|
|
91
|
+
type RunEvidence = Pick<
|
|
92
|
+
ActiveTurn,
|
|
93
|
+
| "completedMessageCount"
|
|
94
|
+
| "streamedMessageEntryIds"
|
|
95
|
+
| "streamedMessageIdentityComplete"
|
|
96
|
+
| "lastCompletedAssistantOutcome"
|
|
97
|
+
| "lastCompletedAssistantEntryId"
|
|
98
|
+
>;
|
|
99
|
+
|
|
100
|
+
/** A run that ended with `isTerminal: false`, kept until another run starts or OMP settles. */
|
|
101
|
+
export type HeldAgentEnd = {
|
|
102
|
+
event: Extract<OmpRpcEvent, { type: "agent_end" }>;
|
|
103
|
+
arrivalSequence: number;
|
|
104
|
+
evidence: RunEvidence;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Hands over the evidence of the run that just ended and starts the next run's evidence empty. */
|
|
108
|
+
export function takeRunEvidence(turn: ActiveTurn): RunEvidence {
|
|
109
|
+
const evidence: RunEvidence = {
|
|
110
|
+
completedMessageCount: turn.completedMessageCount,
|
|
111
|
+
streamedMessageEntryIds: turn.streamedMessageEntryIds.splice(0),
|
|
112
|
+
streamedMessageIdentityComplete: turn.streamedMessageIdentityComplete,
|
|
113
|
+
lastCompletedAssistantOutcome: turn.lastCompletedAssistantOutcome,
|
|
114
|
+
lastCompletedAssistantEntryId: turn.lastCompletedAssistantEntryId,
|
|
115
|
+
};
|
|
116
|
+
turn.completedMessageCount = 0;
|
|
117
|
+
turn.streamedMessageIdentityComplete = true;
|
|
118
|
+
turn.lastCompletedAssistantOutcome = undefined;
|
|
119
|
+
turn.lastCompletedAssistantEntryId = undefined;
|
|
120
|
+
return evidence;
|
|
121
|
+
}
|
|
122
|
+
|
|
85
123
|
export type VoidDeferred = {
|
|
86
124
|
promise: Promise<void>;
|
|
87
125
|
resolve(value?: void | PromiseLike<void>): void;
|
|
@@ -249,6 +287,7 @@ export function createActiveTurn(
|
|
|
249
287
|
usageSampleFloor: 0,
|
|
250
288
|
agentEndPending: false,
|
|
251
289
|
terminalizing: false,
|
|
290
|
+
terminalEmitAttempted: false,
|
|
252
291
|
manualCompactionPending: manualCompaction,
|
|
253
292
|
manualCompaction,
|
|
254
293
|
activitySequence: 0,
|
|
@@ -276,6 +315,15 @@ export function createActiveTurn(
|
|
|
276
315
|
],
|
|
277
316
|
};
|
|
278
317
|
}
|
|
318
|
+
/** A run OMP started without a client prompt: an extension, goal, or background wake. */
|
|
319
|
+
export function createNativeRunTurn(generation: number): ActiveTurn {
|
|
320
|
+
return {
|
|
321
|
+
...createActiveTurn("", "", generation, "native-run"),
|
|
322
|
+
promptResultEmitted: true,
|
|
323
|
+
pendingUsers: [],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
279
327
|
export function classifyTerminalCandidate(
|
|
280
328
|
turn: ActiveTurn,
|
|
281
329
|
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
@@ -286,6 +334,9 @@ export function classifyTerminalCandidate(
|
|
|
286
334
|
if (turn.terminalCorrelation.policy === "initial-turn") {
|
|
287
335
|
return { event, confidence: "initial-turn", arrivalSequence };
|
|
288
336
|
}
|
|
337
|
+
if (turn.terminalCorrelation.policy === "native-run") {
|
|
338
|
+
return { event, confidence: "native-run", arrivalSequence };
|
|
339
|
+
}
|
|
289
340
|
const assistantSequence = turn.terminalCorrelation.evidence.get("current-assistant");
|
|
290
341
|
if (
|
|
291
342
|
turn.terminalCorrelation.policy === "native-command" &&
|