@omercnet/paseo-omp 0.4.0-next.122.1 → 0.4.0-next.124.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.
@@ -7,7 +7,6 @@ import type {
7
7
  ProviderPermissionResponse,
8
8
  ProviderSessionConfig,
9
9
  ProviderToolCallDetail,
10
- ProviderUsage,
11
10
  } from "@getpaseo/plugin/server/provider";
12
11
  import type {
13
12
  OmpOperationalFailure,
@@ -40,7 +39,6 @@ import type {
40
39
  OmpModel,
41
40
  OmpRpcEvent,
42
41
  OmpSessionState,
43
- OmpSessionStats,
44
42
  OmpToolApprovalCancel,
45
43
  OmpToolApprovalRequest,
46
44
  } from "./omp-rpc-protocol";
@@ -64,8 +62,23 @@ import {
64
62
  OmpPublicError,
65
63
  utf8Bytes,
66
64
  } from "./security";
65
+ import { OmpSessionCompaction } from "./session-compaction";
67
66
  import type { OmpSessionDescriptor } from "./session-descriptors";
68
67
  import { validateNativeSessionId } from "./session-descriptors";
68
+ import { isNativeTurnActivity, isPassiveUiMethod, isRuntimeConfigEvent } from "./session-events";
69
+ import {
70
+ type ActiveTurn,
71
+ assistantTerminalOutcome,
72
+ classifyTerminalCandidate,
73
+ createActiveTurn,
74
+ historyTerminalOutcome,
75
+ nativeEntryId,
76
+ type PendingUser,
77
+ type TerminalCandidate,
78
+ terminalOutcome,
79
+ unknownTerminalOutcomeError,
80
+ } from "./session-terminal";
81
+ import { OmpSessionUsage } from "./session-usage";
69
82
  import { OmpSubsessionProjector } from "./subsessions";
70
83
  import {
71
84
  defaultOmpTimelineScheduler,
@@ -235,86 +248,6 @@ async function waitForReplay<T>(operation: Promise<T>, signal: AbortSignal): Pro
235
248
  }
236
249
  }
237
250
 
238
- type PendingUser = {
239
- clientMessageId: string;
240
- text: string;
241
- accepted: boolean;
242
- fallbackOnFinish: boolean;
243
- bufferedEchoes: OmpMessage[];
244
- };
245
- type TerminalCorrelationEvidence = "fresh-native-user" | "current-assistant";
246
-
247
- type TerminalCorrelation = {
248
- policy: "initial-turn" | "ordered-legacy" | "native-command";
249
- evidence: Map<TerminalCorrelationEvidence, number>;
250
- };
251
-
252
- type TerminalCandidate = {
253
- event: Extract<OmpRpcEvent, { type: "agent_end" }>;
254
- arrivalSequence: number;
255
- confidence:
256
- | "keyed"
257
- | "initial-turn"
258
- | "ordered-legacy"
259
- | "native-command"
260
- | "interrupted"
261
- | "ambiguous";
262
- };
263
-
264
- type ActiveTurn = {
265
- turnId: string;
266
- clientMessageId: string;
267
- generation: number;
268
- promptResultEmitted: boolean;
269
- started: boolean;
270
- terminal: boolean;
271
- interrupted: boolean;
272
- starting: boolean;
273
- nativeActivity: boolean;
274
- userEchoObserved: boolean;
275
- localOnlyDisabled: boolean;
276
- localOnlyEligible: boolean;
277
- awaitingPermissionEvidence: boolean;
278
- activitySequence: number;
279
- acknowledged: boolean;
280
- terminalCorrelation: TerminalCorrelation;
281
- replayingBufferedEvents: boolean;
282
- agentInvoked?: boolean;
283
- nativeRequestId?: string;
284
- promptAcceptedEventIndex?: number;
285
- localOnlyTimer?: unknown;
286
- usagePollTimer?: unknown;
287
- usagePoll?: Promise<void>;
288
- usageSampleFloor: number;
289
- manualCompaction: boolean;
290
- manualCompactionPending: boolean;
291
- manualCompactionDeadlineTimer?: unknown;
292
- agentEndPending: boolean;
293
- agentEndRetryTimer?: unknown;
294
- agentEndDeadlineTimer?: unknown;
295
- agentEndCheck?: Promise<void>;
296
- ambiguousTerminalTimer?: unknown;
297
- terminalizing: boolean;
298
- terminalization?: Promise<void>;
299
- terminalOutcome?: TurnOutcome;
300
- operationalTerminalStage?: "failed" | "unresolved";
301
- terminalWake?: VoidDeferred;
302
- steerReady: VoidDeferred;
303
- steersInFlight: number;
304
- deferredAgentEnd?: TerminalCandidate;
305
- activeToolCallIds: Set<string>;
306
- bufferedEvents: OmpRpcEvent[];
307
- pendingUsers: PendingUser[];
308
- userEchoes: OmpMessage[];
309
- userCorrelationActive: boolean;
310
- userLookups: Set<Promise<void>>;
311
- completedMessageCount: number;
312
- streamedMessageEntryIds: string[];
313
- streamedMessageIdentityComplete: boolean;
314
- lastCompletedAssistantOutcome?: AgentEndOutcome;
315
- lastCompletedAssistantEntryId?: string;
316
- };
317
-
318
251
  type PendingAbort = {
319
252
  turn: ActiveTurn;
320
253
  generation: number;
@@ -362,28 +295,6 @@ function permissionFingerprint(request: OmpQuestionRequest): string {
362
295
  return createHash("sha256").update(JSON.stringify(request)).digest("base64url");
363
296
  }
364
297
 
365
- type VoidDeferred = {
366
- promise: Promise<void>;
367
- resolve(value?: void | PromiseLike<void>): void;
368
- reject(reason?: unknown): void;
369
- };
370
-
371
- type TurnOutcome = {
372
- state: "completed" | "failed" | "canceled";
373
- error?: ProviderError;
374
- usageSampled: boolean;
375
- };
376
-
377
- type ActiveCompaction = {
378
- id: string;
379
- trigger: "auto" | "manual";
380
- turnId: string;
381
- generation: number;
382
- retrying: boolean;
383
- action?: string;
384
- preTokens?: number;
385
- };
386
-
387
298
  function providerError(error: unknown, fallback: string): { message: string } {
388
299
  return { message: isOmpPublicError(error) ? error.message : fallback };
389
300
  }
@@ -408,258 +319,6 @@ async function settleSessionCleanup(promises: readonly Promise<void>[]): Promise
408
319
  throw new AggregateError(failures, "OMP session initialization cleanup failed");
409
320
  }
410
321
  }
411
- function nativeEntryId(message: OmpMessage): string | undefined {
412
- return message.entryId;
413
- }
414
-
415
- type AgentEndOutcome = "completed" | "failed" | "canceled";
416
- type AssistantTerminalStatus = AgentEndOutcome | "unavailable";
417
-
418
- function assistantTerminalOutcome(
419
- message: Extract<OmpMessage, { role: "assistant" }>,
420
- ): AgentEndOutcome {
421
- const stopReason = message.stopReason?.toLowerCase();
422
- if (stopReason === "aborted" || stopReason === "canceled" || stopReason === "cancelled") {
423
- return "canceled";
424
- }
425
- return stopReason === "error" || message.errorMessage ? "failed" : "completed";
426
- }
427
-
428
- function lastAssistantStatus(
429
- messages: readonly OmpMessage[],
430
- startIndex = 0,
431
- ): AssistantTerminalStatus {
432
- for (let index = messages.length - 1; index >= startIndex; index -= 1) {
433
- const message = messages[index];
434
- if (message?.role !== "assistant") continue;
435
- return assistantTerminalOutcome(message);
436
- }
437
- return "unavailable";
438
- }
439
-
440
- function terminalOutcome(
441
- event: Extract<OmpRpcEvent, { type: "agent_end" }>,
442
- turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
443
- ): AgentEndOutcome | undefined {
444
- const messages = event.messages;
445
- if (
446
- messages !== undefined &&
447
- (event.messageCount === undefined || messages.length >= event.messageCount)
448
- ) {
449
- const status = lastAssistantStatus(messages);
450
- return status === "unavailable" ? "completed" : status;
451
- }
452
- if (event.messageCount === 0) return "completed";
453
- if (
454
- turn.lastCompletedAssistantOutcome !== undefined &&
455
- (event.messageCount === undefined || turn.completedMessageCount >= event.messageCount)
456
- ) {
457
- return turn.lastCompletedAssistantOutcome;
458
- }
459
- return undefined;
460
- }
461
-
462
- function historyTerminalOutcome(
463
- messages: readonly OmpMessage[],
464
- declaredCount: number,
465
- turn: Pick<
466
- ActiveTurn,
467
- | "streamedMessageEntryIds"
468
- | "streamedMessageIdentityComplete"
469
- | "lastCompletedAssistantOutcome"
470
- | "lastCompletedAssistantEntryId"
471
- >,
472
- retainedMessages: readonly OmpMessage[],
473
- ): AgentEndOutcome | undefined {
474
- if (
475
- messages.length < declaredCount ||
476
- !turn.streamedMessageIdentityComplete ||
477
- turn.streamedMessageEntryIds.length === 0
478
- ) {
479
- return undefined;
480
- }
481
- const startIndex = messages.length - declaredCount;
482
- let historyIndex = startIndex;
483
- for (const entryId of turn.streamedMessageEntryIds) {
484
- while (historyIndex < messages.length) {
485
- const historyMessage = messages[historyIndex];
486
- if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
487
- historyIndex += 1;
488
- }
489
- if (historyIndex >= messages.length) return undefined;
490
- const correlated = messages[historyIndex];
491
- if (
492
- entryId === turn.lastCompletedAssistantEntryId &&
493
- (correlated?.role !== "assistant" ||
494
- assistantTerminalOutcome(correlated) !== turn.lastCompletedAssistantOutcome)
495
- )
496
- return undefined;
497
- historyIndex += 1;
498
- }
499
- historyIndex = startIndex;
500
- for (const retained of retainedMessages) {
501
- const entryId = nativeEntryId(retained);
502
- if (!entryId) return undefined;
503
- while (historyIndex < messages.length) {
504
- const historyMessage = messages[historyIndex];
505
- if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
506
- historyIndex += 1;
507
- }
508
- if (historyIndex >= messages.length) return undefined;
509
- const correlated = messages[historyIndex];
510
- if (!correlated || correlated.role !== retained.role) return undefined;
511
- if (
512
- retained.role === "assistant" &&
513
- correlated.role === "assistant" &&
514
- assistantTerminalOutcome(retained) !== assistantTerminalOutcome(correlated)
515
- )
516
- return undefined;
517
- historyIndex += 1;
518
- }
519
- const status = lastAssistantStatus(messages, startIndex);
520
- return status === "unavailable" ? undefined : status;
521
- }
522
-
523
- function unknownTerminalOutcomeError(
524
- event: Extract<OmpRpcEvent, { type: "agent_end" }>,
525
- turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
526
- ): string {
527
- const retainedMessages = event.messages?.length ?? 0;
528
- const retainedStatus = event.messages ? lastAssistantStatus(event.messages) : "unavailable";
529
- const lastStatus =
530
- retainedStatus === "unavailable"
531
- ? (turn.lastCompletedAssistantOutcome ?? "unavailable")
532
- : retainedStatus;
533
- return (
534
- "OMP agent_end omitted terminal messages; outcome is unknown " +
535
- `(declaredCount=${event.messageCount ?? "unavailable"}, ` +
536
- `observedCount=${turn.completedMessageCount}, ` +
537
- `retainedTerminalMessages=${retainedMessages}, lastAssistantStatus=${lastStatus})`
538
- );
539
- }
540
-
541
- function isNativeTurnActivity(event: OmpRpcEvent): boolean {
542
- if (
543
- event.type === "agent_start" ||
544
- event.type === "turn_start" ||
545
- event.type === "turn_end" ||
546
- event.type === "agent_end" ||
547
- event.type === "auto_compaction_start" ||
548
- event.type === "auto_compaction_end"
549
- ) {
550
- return true;
551
- }
552
- if (
553
- event.type === "message_start" ||
554
- event.type === "message_update" ||
555
- event.type === "message_end"
556
- ) {
557
- return event.message.role === "assistant";
558
- }
559
- return event.type.startsWith("tool_execution_");
560
- }
561
- function isRuntimeConfigEvent(event: OmpRpcEvent): boolean {
562
- return (
563
- event.type === "model_changed" ||
564
- event.type === "thinking_level_changed" ||
565
- event.type === "retry_fallback_applied" ||
566
- event.type === "retry_fallback_succeeded"
567
- );
568
- }
569
-
570
- function isPassiveUiMethod(method: string): boolean {
571
- return (
572
- method === "cancel" ||
573
- method === "notify" ||
574
- method === "open_url" ||
575
- method === "setStatus" ||
576
- method === "setWidget" ||
577
- method === "setTitle" ||
578
- method === "set_editor_text"
579
- );
580
- }
581
- function createActiveTurn(
582
- clientMessageId: string,
583
- text: string,
584
- generation: number,
585
- terminalPolicy: TerminalCorrelation["policy"],
586
- manualCompaction = false,
587
- ): ActiveTurn {
588
- return {
589
- turnId: randomUUID(),
590
- clientMessageId,
591
- agentInvoked: undefined,
592
- generation,
593
- promptResultEmitted: false,
594
- started: false,
595
- terminal: false,
596
- awaitingPermissionEvidence: false,
597
- interrupted: false,
598
- starting: true,
599
- nativeActivity: false,
600
- userEchoObserved: false,
601
- localOnlyDisabled: false,
602
- localOnlyEligible: false,
603
- usageSampleFloor: 0,
604
- agentEndPending: false,
605
- terminalizing: false,
606
- manualCompactionPending: manualCompaction,
607
- manualCompaction,
608
- activitySequence: 0,
609
- acknowledged: false,
610
- terminalCorrelation: { policy: terminalPolicy, evidence: new Map() },
611
- replayingBufferedEvents: false,
612
- steersInFlight: 0,
613
- activeToolCallIds: new Set(),
614
- steerReady: Promise.withResolvers<void>(),
615
- userCorrelationActive: false,
616
- userLookups: new Set(),
617
- userEchoes: [],
618
- completedMessageCount: 0,
619
- bufferedEvents: [],
620
- streamedMessageEntryIds: [],
621
- streamedMessageIdentityComplete: true,
622
- pendingUsers: [
623
- {
624
- clientMessageId,
625
- text,
626
- accepted: true,
627
- fallbackOnFinish: true,
628
- bufferedEchoes: [],
629
- },
630
- ],
631
- };
632
- }
633
- function classifyTerminalCandidate(
634
- turn: ActiveTurn,
635
- event: Extract<OmpRpcEvent, { type: "agent_end" }>,
636
- arrivalSequence = turn.activitySequence,
637
- ): TerminalCandidate {
638
- if (event.requestId !== undefined) return { event, confidence: "keyed", arrivalSequence };
639
- if (turn.agentInvoked === false) return { event, confidence: "ambiguous", arrivalSequence };
640
- if (turn.terminalCorrelation.policy === "initial-turn") {
641
- return { event, confidence: "initial-turn", arrivalSequence };
642
- }
643
- const assistantSequence = turn.terminalCorrelation.evidence.get("current-assistant");
644
- if (
645
- turn.terminalCorrelation.policy === "native-command" &&
646
- assistantSequence !== undefined &&
647
- assistantSequence <= arrivalSequence
648
- ) {
649
- return { event, confidence: "native-command", arrivalSequence };
650
- }
651
- const userSequence = turn.terminalCorrelation.evidence.get("fresh-native-user");
652
- if (
653
- userSequence !== undefined &&
654
- assistantSequence !== undefined &&
655
- userSequence < assistantSequence &&
656
- assistantSequence <= arrivalSequence
657
- ) {
658
- return { event, confidence: "ordered-legacy", arrivalSequence };
659
- }
660
- return { event, confidence: "ambiguous", arrivalSequence };
661
- }
662
-
663
322
  export class OmpProviderSession {
664
323
  readonly id: string;
665
324
  readonly cwd: string;
@@ -695,19 +354,8 @@ export class OmpProviderSession {
695
354
  private configRevision = 0;
696
355
  private recoveryUsesNativeConfig = false;
697
356
  private activeAbort: PendingAbort | null = null;
698
- private usageEpoch = 0;
699
- private usageSequence = 0;
700
- private usageSample: {
701
- turn: ActiveTurn;
702
- generation: number;
703
- runtime: OmpRuntimeSession;
704
- epoch: number;
705
- sequence: number;
706
- promise: Promise<OmpSessionState | undefined>;
707
- } | null = null;
708
- private activeCompaction: ActiveCompaction | null = null;
709
- private discardedCompactionEnds = 0;
710
- private lastUsage: ProviderUsage | null = null;
357
+ private readonly usage: OmpSessionUsage;
358
+ private readonly compaction: OmpSessionCompaction;
711
359
  private revertInFlight = false;
712
360
  private runtimeTurnCompleted = false;
713
361
  private commandCatalog: OmpAvailableCommand[];
@@ -751,6 +399,26 @@ export class OmpProviderSession {
751
399
  this.id = id;
752
400
  this.cwd = config.cwd;
753
401
  this.scheduler = scheduler;
402
+ this.usage = new OmpSessionUsage(
403
+ id,
404
+ emit,
405
+ scheduler,
406
+ () => ({
407
+ closed: this.closed,
408
+ runtimeDead: this.runtimeDead !== null,
409
+ generation: this.generation,
410
+ runtime: this.runtime,
411
+ activeTurn: this.activeTurn,
412
+ }),
413
+ (turn) =>
414
+ !this.closed &&
415
+ !this.runtimeDead &&
416
+ !turn.terminal &&
417
+ !turn.terminalizing &&
418
+ !turn.agentEndPending &&
419
+ turn.generation === this.generation &&
420
+ this.activeTurn === turn,
421
+ );
754
422
  this.dataFilter = new OmpPublicDataSerializer(outputRedactionValues);
755
423
  this.nativeModelsByPublicId = nativeModelsByPublicId;
756
424
  this.commandCatalog = commandCatalog;
@@ -764,6 +432,13 @@ export class OmpProviderSession {
764
432
  capabilities.includes("timeline.plugin"),
765
433
  hostTools.labels,
766
434
  );
435
+ this.compaction = new OmpSessionCompaction(
436
+ id,
437
+ emit,
438
+ this.projector,
439
+ this.dataFilter,
440
+ this.usage,
441
+ );
767
442
  this.subsessions = capabilities.includes("session.subsession")
768
443
  ? new OmpSubsessionProjector(
769
444
  id,
@@ -1105,289 +780,6 @@ export class OmpProviderSession {
1105
780
  });
1106
781
  }
1107
782
 
1108
- private usageFrom(
1109
- state: OmpSessionState | undefined,
1110
- stats: OmpSessionStats | undefined,
1111
- ): ProviderUsage | undefined {
1112
- if (!state?.contextUsage && !stats) return undefined;
1113
- const stateContext = state?.contextUsage;
1114
- const statsContext = stats?.contextUsage;
1115
- const modelCapacity = state?.model?.contextWindow;
1116
- const inputTokens = stats?.tokens?.input;
1117
- const cachedInputTokens = stats?.tokens?.cacheRead;
1118
- const outputTokens = stats?.tokens?.output;
1119
- const totalCostUsd = stats?.cost;
1120
- const contextTokens =
1121
- typeof stateContext?.tokens === "number"
1122
- ? stateContext.tokens
1123
- : typeof statsContext?.tokens === "number"
1124
- ? statsContext.tokens
1125
- : undefined;
1126
- const contextWindow =
1127
- typeof stateContext?.contextWindow === "number" && stateContext.contextWindow > 0
1128
- ? stateContext.contextWindow
1129
- : typeof statsContext?.contextWindow === "number" && statsContext.contextWindow > 0
1130
- ? statsContext.contextWindow
1131
- : typeof modelCapacity === "number" && modelCapacity > 0
1132
- ? modelCapacity
1133
- : undefined;
1134
- const usage: ProviderUsage = {
1135
- ...(typeof inputTokens === "number" ? { inputTokens } : {}),
1136
- ...(typeof cachedInputTokens === "number" ? { cachedInputTokens } : {}),
1137
- ...(typeof outputTokens === "number" ? { outputTokens } : {}),
1138
- ...(typeof totalCostUsd === "number" ? { totalCostUsd } : {}),
1139
- ...(contextTokens !== undefined ? { contextWindowUsedTokens: contextTokens } : {}),
1140
- ...(contextWindow !== undefined ? { contextWindowMaxTokens: contextWindow } : {}),
1141
- };
1142
- return Object.keys(usage).length > 0 ? usage : undefined;
1143
- }
1144
-
1145
- private ownsUsageSample(
1146
- turn: ActiveTurn,
1147
- generation: number,
1148
- runtime: OmpRuntimeSession,
1149
- ): boolean {
1150
- return (
1151
- !this.closed &&
1152
- !this.runtimeDead &&
1153
- !turn.terminal &&
1154
- generation === this.generation &&
1155
- runtime === this.runtime &&
1156
- this.activeTurn === turn
1157
- );
1158
- }
1159
-
1160
- private publishUsageSnapshot(
1161
- turn: ActiveTurn,
1162
- minimumEpoch = this.usageEpoch,
1163
- minimumSequence = turn.usageSampleFloor,
1164
- ): Promise<OmpSessionState | undefined> {
1165
- const generation = turn.generation;
1166
- const runtime = this.runtime;
1167
- if (!this.ownsUsageSample(turn, generation, runtime)) return Promise.resolve(undefined);
1168
- const current = this.usageSample;
1169
- if (current) {
1170
- if (
1171
- current.turn === turn &&
1172
- current.generation === generation &&
1173
- current.runtime === runtime &&
1174
- current.epoch >= minimumEpoch &&
1175
- current.sequence >= minimumSequence
1176
- ) {
1177
- return current.promise;
1178
- }
1179
- return current.promise.then(() => {
1180
- if (!this.ownsUsageSample(turn, generation, runtime)) return undefined;
1181
- return this.publishUsageSnapshot(turn, minimumEpoch, minimumSequence);
1182
- });
1183
- }
1184
- const epoch = this.usageEpoch;
1185
- const sequence = ++this.usageSequence;
1186
- const promise = Promise.allSettled([runtime.getState(), runtime.getSessionStats()]).then(
1187
- ([stateResult, statsResult]) => {
1188
- if (
1189
- !this.ownsUsageSample(turn, generation, runtime) ||
1190
- epoch !== this.usageEpoch ||
1191
- epoch < minimumEpoch ||
1192
- sequence < turn.usageSampleFloor ||
1193
- sequence < minimumSequence
1194
- ) {
1195
- return undefined;
1196
- }
1197
- const state = stateResult.status === "fulfilled" ? stateResult.value : undefined;
1198
- const stats = statsResult.status === "fulfilled" ? statsResult.value : undefined;
1199
- const usage = this.usageFrom(state, stats);
1200
- if (usage) {
1201
- this.lastUsage = usage;
1202
- this.emit({ type: "session.usage", sessionId: this.id, turnId: turn.turnId, usage });
1203
- }
1204
- return state;
1205
- },
1206
- );
1207
- this.usageSample = { turn, generation, runtime, epoch, sequence, promise };
1208
- void promise.finally(() => {
1209
- if (this.usageSample?.promise === promise) this.usageSample = null;
1210
- });
1211
- return promise;
1212
- }
1213
-
1214
- private async boundedUsageSnapshot(
1215
- turn: ActiveTurn,
1216
- timeoutMs: number,
1217
- ): Promise<OmpSessionState | undefined> {
1218
- const timeout = Promise.withResolvers<undefined>();
1219
- const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
1220
- try {
1221
- return await Promise.race([this.publishUsageSnapshot(turn), timeout.promise]);
1222
- } finally {
1223
- this.scheduler.clear(timer);
1224
- }
1225
- }
1226
-
1227
- private async boundedTerminalState(
1228
- turn: ActiveTurn,
1229
- timeoutMs: number,
1230
- ): Promise<OmpSessionState | undefined> {
1231
- const generation = turn.generation;
1232
- const runtime = this.runtime;
1233
- const timeout = Promise.withResolvers<undefined>();
1234
- const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
1235
- try {
1236
- const state = await Promise.race([runtime.getState(), timeout.promise]);
1237
- return this.ownsUsageSample(turn, generation, runtime) ? state : undefined;
1238
- } catch {
1239
- return undefined;
1240
- } finally {
1241
- this.scheduler.clear(timer);
1242
- }
1243
- }
1244
-
1245
- private isActiveTurn(turn: ActiveTurn): boolean {
1246
- return (
1247
- !this.closed &&
1248
- !this.runtimeDead &&
1249
- !turn.terminal &&
1250
- !turn.terminalizing &&
1251
- !turn.agentEndPending &&
1252
- turn.generation === this.generation &&
1253
- this.activeTurn === turn
1254
- );
1255
- }
1256
-
1257
- private scheduleUsagePoll(turn: ActiveTurn, delayMs = USAGE_POLL_MS): void {
1258
- if (!this.isActiveTurn(turn) || turn.usagePollTimer !== undefined) return;
1259
- turn.usagePollTimer = this.scheduler.set(() => {
1260
- turn.usagePollTimer = undefined;
1261
- this.pollUsage(turn);
1262
- }, delayMs);
1263
- }
1264
-
1265
- private pollUsage(turn: ActiveTurn): void {
1266
- if (!this.isActiveTurn(turn) || turn.usagePoll) return;
1267
- const poll = this.publishUsageSnapshot(turn).then(() => undefined);
1268
- turn.usagePoll = poll;
1269
- void poll.finally(() => {
1270
- if (turn.usagePoll === poll) turn.usagePoll = undefined;
1271
- this.scheduleUsagePoll(turn);
1272
- });
1273
- }
1274
-
1275
- private stopUsagePoll(turn: ActiveTurn): void {
1276
- if (turn.usagePollTimer === undefined) return;
1277
- this.scheduler.clear(turn.usagePollTimer);
1278
- turn.usagePollTimer = undefined;
1279
- }
1280
-
1281
- private startCompaction(turn: ActiveTurn, trigger: "auto" | "manual", action?: string): void {
1282
- if (this.discardedCompactionEnds > 0) {
1283
- this.discardedCompactionEnds += 1;
1284
- return;
1285
- }
1286
- const active = this.activeCompaction;
1287
- if (active && active.trigger === trigger && active.action === action) {
1288
- active.retrying = false;
1289
- return;
1290
- }
1291
- if (active) {
1292
- this.retireCompaction("OMP emitted overlapping compactions");
1293
- this.discardedCompactionEnds = 2;
1294
- return;
1295
- }
1296
- const operation: ActiveCompaction = {
1297
- id: randomUUID(),
1298
- trigger,
1299
- turnId: turn.turnId,
1300
- generation: turn.generation,
1301
- action,
1302
- retrying: false,
1303
- preTokens: this.lastUsage?.contextWindowUsedTokens,
1304
- };
1305
- this.activeCompaction = operation;
1306
- this.projector.flush(true);
1307
- this.emit({
1308
- type: "timeline.item",
1309
- sessionId: this.id,
1310
- item: {
1311
- id: operation.id,
1312
- type: "compaction",
1313
- status: "loading",
1314
- trigger,
1315
- ...(operation.preTokens !== undefined ? { preTokens: operation.preTokens } : {}),
1316
- },
1317
- });
1318
- }
1319
-
1320
- private finishCompaction(
1321
- state: "completed" | "failed" | "canceled" | "skipped",
1322
- options: { tokensBefore?: number | null; message?: string } = {},
1323
- ): void {
1324
- const operation = this.activeCompaction;
1325
- if (!operation) return;
1326
- this.activeCompaction = null;
1327
- this.projector.flush(true);
1328
- if (state === "completed") {
1329
- this.usageEpoch += 1;
1330
- const staleSample = this.usageSample;
1331
- if (staleSample && staleSample.epoch < this.usageEpoch) {
1332
- this.usageSample = null;
1333
- staleSample.turn.usagePoll = undefined;
1334
- }
1335
- this.lastUsage = null;
1336
- }
1337
- if (state !== "completed") {
1338
- const defaultMessage =
1339
- state === "failed"
1340
- ? "OMP compaction failed"
1341
- : state === "canceled"
1342
- ? "OMP compaction canceled"
1343
- : "OMP compaction skipped";
1344
- this.emit({
1345
- type: "timeline.item",
1346
- sessionId: this.id,
1347
- item: {
1348
- id: operation.id,
1349
- type: "notification",
1350
- level: state === "failed" ? "error" : "info",
1351
- message: this.dataFilter.text(options.message ?? defaultMessage, 4_096),
1352
- },
1353
- });
1354
- return;
1355
- }
1356
- const tokensBefore = options.tokensBefore ?? operation.preTokens;
1357
- this.emit({
1358
- type: "timeline.item",
1359
- sessionId: this.id,
1360
- item: {
1361
- id: operation.id,
1362
- type: "compaction",
1363
- status: "completed",
1364
- trigger: operation.trigger,
1365
- ...(tokensBefore !== undefined ? { preTokens: tokensBefore } : {}),
1366
- },
1367
- });
1368
- }
1369
-
1370
- private retireCompaction(message: string): void {
1371
- const operation = this.activeCompaction;
1372
- if (!operation) return;
1373
- this.activeCompaction = null;
1374
- this.emit({
1375
- type: "timeline.item",
1376
- sessionId: this.id,
1377
- item: {
1378
- id: operation.id,
1379
- type: "compaction",
1380
- status: "completed",
1381
- trigger: operation.trigger,
1382
- },
1383
- });
1384
- this.emit({
1385
- type: "timeline.item",
1386
- sessionId: this.id,
1387
- item: { id: `${operation.id}:error`, type: "error", message },
1388
- });
1389
- }
1390
-
1391
783
  private async replayHistory(preferPersistedTranscript = false): Promise<void> {
1392
784
  if (!this.runtime.canReplayHistory) {
1393
785
  throw new OmpPublicError("OMP session history cannot be replayed safely");
@@ -1780,8 +1172,8 @@ export class OmpProviderSession {
1780
1172
  turn.starting = false;
1781
1173
  turn.acknowledged = true;
1782
1174
  this.startTurn(turn, false);
1783
- this.startCompaction(turn, "manual");
1784
- this.pollUsage(turn);
1175
+ this.compaction.start(turn, "manual");
1176
+ this.usage.poll(turn);
1785
1177
  const bufferedEvents = turn.bufferedEvents.splice(0);
1786
1178
  turn.replayingBufferedEvents = true;
1787
1179
  for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
@@ -1872,7 +1264,7 @@ export class OmpProviderSession {
1872
1264
  else {
1873
1265
  turn.steerReady.resolve();
1874
1266
  turn.terminal = true;
1875
- this.finishCompaction("failed", { message: "OMP prompt failed" });
1267
+ this.compaction.finish("failed", { message: "OMP prompt failed" });
1876
1268
  this.resolveTurnPermissions(turn.turnId);
1877
1269
  this.projector.finishTurn(turn.turnId);
1878
1270
  if (this.activeTurn === turn) this.activeTurn = null;
@@ -2045,7 +1437,7 @@ export class OmpProviderSession {
2045
1437
  const result = await compaction;
2046
1438
  turn.manualCompactionPending = false;
2047
1439
  if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
2048
- this.finishCompaction("completed", { tokensBefore: result.tokensBefore });
1440
+ this.compaction.finish("completed", { tokensBefore: result.tokensBefore });
2049
1441
  await this.finishTurn(turn, turn.interrupted ? "canceled" : "completed");
2050
1442
  } catch (error) {
2051
1443
  if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
@@ -2053,7 +1445,7 @@ export class OmpProviderSession {
2053
1445
  const raw = providerError(error, "OMP compaction failed");
2054
1446
  const failure = { message: this.dataFilter.text(raw.message, 4_096) };
2055
1447
  const state = turn.interrupted ? "canceled" : "failed";
2056
- this.finishCompaction(state, { message: failure.message });
1448
+ this.compaction.finish(state, { message: failure.message });
2057
1449
  await this.finishTurn(turn, state, state === "failed" ? failure : undefined, true, true);
2058
1450
  }
2059
1451
  }
@@ -2630,11 +2022,11 @@ export class OmpProviderSession {
2630
2022
  if (turn.started) await this.finishTurn(turn, "canceled", undefined, true, true);
2631
2023
  else {
2632
2024
  this.settleUnstartedTurn(turn);
2633
- this.finishCompaction("canceled");
2025
+ this.compaction.finish("canceled");
2634
2026
  }
2635
2027
  }
2636
2028
  this.imageMaterializer.clear();
2637
- this.finishCompaction("canceled");
2029
+ this.compaction.finish("canceled");
2638
2030
  this.resolveAllPermissions(true);
2639
2031
  this.closed = true;
2640
2032
  this.configRefreshAttempts = 0;
@@ -2794,7 +2186,7 @@ export class OmpProviderSession {
2794
2186
  }
2795
2187
  if (this.subsessions) await recovered.setSubagentSubscription("events");
2796
2188
  this.generation += 1;
2797
- this.lastUsage = null;
2189
+ this.usage.clearLatest();
2798
2190
  this.runtimeDead = null;
2799
2191
  this.runtimeDisposal = null;
2800
2192
  unsubscribeBootstrap();
@@ -3141,43 +2533,13 @@ export class OmpProviderSession {
3141
2533
  if (event.type === "auto_compaction_start") {
3142
2534
  turn.nativeActivity = true;
3143
2535
  this.cancelLocalOnlyCompletion(turn);
3144
- this.startCompaction(turn, "auto", event.action);
2536
+ this.compaction.start(turn, "auto", event.action);
3145
2537
  return;
3146
2538
  }
3147
2539
  if (event.type === "auto_compaction_end") {
3148
- if (this.discardedCompactionEnds > 0) {
3149
- this.discardedCompactionEnds -= 1;
3150
- return;
3151
- }
3152
- const operation = this.activeCompaction;
3153
- if (
3154
- operation?.trigger !== "auto" ||
3155
- operation.turnId !== turn.turnId ||
3156
- operation.generation !== turn.generation
3157
- ) {
3158
- return;
3159
- }
3160
- if (event.action !== undefined && operation.action !== event.action) {
3161
- this.retireCompaction("OMP emitted overlapping compactions");
3162
- this.discardedCompactionEnds = 1;
3163
- return;
3164
- }
3165
- if (event.willRetry) {
3166
- operation.retrying = true;
3167
- return;
2540
+ if (this.compaction.handleAutoEnd(turn, event)) {
2541
+ this.usage.schedulePoll(turn, USAGE_REFRESH_MS);
3168
2542
  }
3169
- const state = event.aborted
3170
- ? "canceled"
3171
- : event.errorMessage
3172
- ? "failed"
3173
- : event.skipped
3174
- ? "skipped"
3175
- : "completed";
3176
- this.finishCompaction(state, {
3177
- tokensBefore: event.result?.tokensBefore ?? event.result?.preTokens,
3178
- message: event.errorMessage,
3179
- });
3180
- this.scheduleUsagePoll(turn, USAGE_REFRESH_MS);
3181
2543
  return;
3182
2544
  }
3183
2545
  if (event.type === "agent_end" && turn.manualCompactionPending) return;
@@ -4324,7 +3686,7 @@ export class OmpProviderSession {
4324
3686
  await Promise.allSettled(turn.userLookups);
4325
3687
  this.refreshAmbiguousTerminal(turn);
4326
3688
  if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
4327
- const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
3689
+ const state = await this.usage.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4328
3690
  if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
4329
3691
  if (!state) {
4330
3692
  turn.operationalTerminalStage = "unresolved";
@@ -4354,10 +3716,7 @@ export class OmpProviderSession {
4354
3716
  ) {
4355
3717
  return;
4356
3718
  }
4357
- turn.usageSampleFloor = this.usageSequence + 1;
4358
- if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
4359
- this.usageSample = null;
4360
- }
3719
+ this.usage.invalidateBeforeNextSample(turn);
4361
3720
  this.publishPendingUsers(turn);
4362
3721
  await this.finishTurn(turn, "completed", undefined, false, false, true);
4363
3722
  }
@@ -4386,7 +3745,7 @@ export class OmpProviderSession {
4386
3745
  this.beginTerminalization(turn, replacement);
4387
3746
  return;
4388
3747
  }
4389
- this.pollUsage(turn);
3748
+ this.usage.poll(turn);
4390
3749
  }
4391
3750
 
4392
3751
  private beginTerminalization(turn: ActiveTurn, candidate: TerminalCandidate): void {
@@ -4400,11 +3759,8 @@ export class OmpProviderSession {
4400
3759
  }
4401
3760
  turn.terminalizing = true;
4402
3761
  this.cancelAmbiguousTerminal(turn);
4403
- turn.usageSampleFloor = this.usageSequence + 1;
4404
- if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
4405
- this.usageSample = null;
4406
- }
4407
- this.stopUsagePoll(turn);
3762
+ this.usage.invalidateBeforeNextSample(turn);
3763
+ this.usage.stopPoll(turn);
4408
3764
  turn.agentEndDeadlineTimer = this.scheduler.set(() => {
4409
3765
  turn.agentEndDeadlineTimer = undefined;
4410
3766
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
@@ -4496,14 +3852,14 @@ export class OmpProviderSession {
4496
3852
  ) {
4497
3853
  this.resetAgentEndProbe(turn);
4498
3854
  turn.deferredAgentEnd = candidate;
4499
- this.pollUsage(turn);
3855
+ this.usage.poll(turn);
4500
3856
  return;
4501
3857
  }
4502
3858
  if (turn.interrupted) {
4503
3859
  await this.completeAgentEnd(turn, candidate.event);
4504
3860
  return;
4505
3861
  }
4506
- const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
3862
+ const state = await this.usage.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4507
3863
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
4508
3864
  if (!turn.interrupted && this.subsessions?.hasActiveChildren()) {
4509
3865
  this.resetAgentEndProbe(turn);
@@ -4516,7 +3872,7 @@ export class OmpProviderSession {
4516
3872
  ) {
4517
3873
  this.resetAgentEndProbe(turn);
4518
3874
  turn.deferredAgentEnd = candidate;
4519
- this.pollUsage(turn);
3875
+ this.usage.poll(turn);
4520
3876
  return;
4521
3877
  }
4522
3878
  if (state) {
@@ -4558,7 +3914,7 @@ export class OmpProviderSession {
4558
3914
  !turn.interrupted &&
4559
3915
  this.activeTurn === turn
4560
3916
  ) {
4561
- const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
3917
+ const state = await this.usage.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4562
3918
  if (
4563
3919
  turn.terminal ||
4564
3920
  this.activeTurn !== turn ||
@@ -4636,7 +3992,7 @@ export class OmpProviderSession {
4636
3992
  turn.steerReady.resolve();
4637
3993
  turn.starting = false;
4638
3994
  turn.terminal = true;
4639
- this.stopUsagePoll(turn);
3995
+ this.usage.stopPoll(turn);
4640
3996
  this.cancelAmbiguousTerminal(turn);
4641
3997
  this.resolveTurnPermissions(turn.turnId);
4642
3998
  this.projector.finishTurn(turn.turnId);
@@ -4663,7 +4019,7 @@ export class OmpProviderSession {
4663
4019
  turn.starting = false;
4664
4020
  turn.steerReady.resolve();
4665
4021
  this.emit({ type: "session.turn", sessionId: this.id, turnId: turn.turnId, state: "started" });
4666
- if (pollUsage) this.pollUsage(turn);
4022
+ if (pollUsage) this.usage.poll(turn);
4667
4023
  }
4668
4024
  private finishTurn(
4669
4025
  turn: ActiveTurn,
@@ -4693,7 +4049,7 @@ export class OmpProviderSession {
4693
4049
  turn.agentEndPending = false;
4694
4050
  this.cancelLocalOnlyCompletion(turn);
4695
4051
  this.cancelAmbiguousTerminal(turn);
4696
- this.stopUsagePoll(turn);
4052
+ this.usage.stopPoll(turn);
4697
4053
  if (turn.manualCompactionDeadlineTimer !== undefined) {
4698
4054
  this.scheduler.clear(turn.manualCompactionDeadlineTimer);
4699
4055
  turn.manualCompactionDeadlineTimer = undefined;
@@ -4712,7 +4068,7 @@ export class OmpProviderSession {
4712
4068
  const terminalization = (async () => {
4713
4069
  if (!turn.terminalOutcome?.usageSampled && !this.closed && !this.runtimeDead) {
4714
4070
  await Promise.race([
4715
- this.boundedUsageSnapshot(turn, FINAL_USAGE_WAIT_MS),
4071
+ this.usage.boundedSnapshot(turn, FINAL_USAGE_WAIT_MS),
4716
4072
  wake.promise.then(() => undefined),
4717
4073
  ]);
4718
4074
  }
@@ -4739,11 +4095,11 @@ export class OmpProviderSession {
4739
4095
  }
4740
4096
  this.imageMaterializer.clear();
4741
4097
  turn.terminal = true;
4742
- if (this.usageSample?.turn === turn) this.usageSample = null;
4743
- if (!preserveCompactions && this.activeCompaction) {
4744
- if (outcome.state === "canceled") this.finishCompaction("canceled");
4098
+ this.usage.clearSample(turn);
4099
+ if (!preserveCompactions && this.compaction.active) {
4100
+ if (outcome.state === "canceled") this.compaction.finish("canceled");
4745
4101
  else {
4746
- this.finishCompaction("failed", {
4102
+ this.compaction.finish("failed", {
4747
4103
  message: outcome.error?.message ?? "OMP compaction ended without a terminal result",
4748
4104
  });
4749
4105
  }
@@ -4780,15 +4136,14 @@ export class OmpProviderSession {
4780
4136
  ): void {
4781
4137
  this.imageMaterializer.clear();
4782
4138
  if (this.closed || this.runtimeDead) return;
4783
- this.discardedCompactionEnds = 0;
4139
+ this.compaction.resetDiscardedEnds();
4784
4140
  this.resolveAllPermissions();
4785
4141
  this.recoveryUsesNativeConfig ||=
4786
4142
  this.configRefreshInFlight !== null || this.configRefreshDirty || this.configMutationInFlight;
4787
4143
  const turn = this.activeTurn;
4788
- if (turn) this.stopUsagePoll(turn);
4789
- this.usageSample = null;
4790
- this.finishCompaction(compactionState, { message });
4791
- this.lastUsage = null;
4144
+ if (turn) this.usage.stopPoll(turn);
4145
+ this.usage.reset();
4146
+ this.compaction.finish(compactionState, { message });
4792
4147
  this.generation += 1;
4793
4148
  this.projector.resetRuntimeGeneration("OMP runtime ended during compaction");
4794
4149
  this.runtimeDead = message;