@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.
@@ -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
- // Count caps only bound memory; the byte and time budgets bound scan work. OMP keeps one
14
- // directory per workspace, so these must exceed real session trees or lookups silently miss.
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(root, budget, async (file) => {
485
- const fileName = basename(file);
486
- const stem = fileName.slice(0, -".jsonl".length);
487
- if (requestedId && !stem.endsWith(`_${requestedId}`)) return true;
488
- const descriptor = await parseDescriptor(file, budget);
489
- if (
490
- !descriptor ||
491
- !stem.endsWith(`_${descriptor.id}`) ||
492
- (options.cwd !== undefined && descriptor.cwd !== options.cwd)
493
- )
494
- return !budgetExceeded(budget);
495
- if (
496
- query &&
497
- !descriptor.id.toLowerCase().includes(query) &&
498
- !descriptor.title?.toLowerCase().includes(query) &&
499
- !descriptor.firstPromptPreview?.toLowerCase().includes(query) &&
500
- !descriptor.lastPromptPreview?.toLowerCase().includes(query)
501
- ) {
502
- return !budgetExceeded(budget);
503
- }
504
- retainNewest(matches, descriptor, retentionLimit);
505
- return !requestedId || matches.length < 2;
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" &&