@omercnet/paseo-omp 0.4.0-next.123.1 → 0.4.0-next.125.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.
@@ -1,13 +1,10 @@
1
- import { createHash, randomUUID } from "node:crypto";
1
+ import { randomUUID } from "node:crypto";
2
2
  import type {
3
3
  ProviderConfigState,
4
4
  ProviderError,
5
5
  ProviderEvent,
6
6
  ProviderInput,
7
- ProviderPermissionResponse,
8
7
  ProviderSessionConfig,
9
- ProviderToolCallDetail,
10
- ProviderUsage,
11
8
  } from "@getpaseo/plugin/server/provider";
12
9
  import type {
13
10
  OmpOperationalFailure,
@@ -35,14 +32,10 @@ import type {
35
32
  OmpAvailableCommand,
36
33
  OmpBranchResult,
37
34
  OmpCompactionResult,
38
- OmpExtensionUiResponse,
39
35
  OmpMessage,
40
36
  OmpModel,
41
37
  OmpRpcEvent,
42
38
  OmpSessionState,
43
- OmpSessionStats,
44
- OmpToolApprovalCancel,
45
- OmpToolApprovalRequest,
46
39
  } from "./omp-rpc-protocol";
47
40
  import { OmpRpcRequestRejectedError } from "./omp-rpc-transport";
48
41
  import {
@@ -64,9 +57,11 @@ import {
64
57
  OmpPublicError,
65
58
  utf8Bytes,
66
59
  } from "./security";
60
+ import { OmpSessionCompaction } from "./session-compaction";
67
61
  import type { OmpSessionDescriptor } from "./session-descriptors";
68
62
  import { validateNativeSessionId } from "./session-descriptors";
69
63
  import { isNativeTurnActivity, isPassiveUiMethod, isRuntimeConfigEvent } from "./session-events";
64
+ import { OmpSessionPermissions } from "./session-permissions";
70
65
  import {
71
66
  type ActiveTurn,
72
67
  assistantTerminalOutcome,
@@ -79,6 +74,7 @@ import {
79
74
  terminalOutcome,
80
75
  unknownTerminalOutcomeError,
81
76
  } from "./session-terminal";
77
+ import { OmpSessionUsage } from "./session-usage";
82
78
  import { OmpSubsessionProjector } from "./subsessions";
83
79
  import {
84
80
  defaultOmpTimelineScheduler,
@@ -96,10 +92,6 @@ type NativeSessionTransition = (previousSessionId: string, nextSessionId: string
96
92
  type RewindCleanupQuarantine = (cleanup: Promise<void>) => void;
97
93
  type RewindSessionRetirement = () => void;
98
94
  type SessionPermissionInput = Extract<ProviderInput, { type: "session.permission" }>;
99
- type OmpQuestionRequest = Extract<
100
- Extract<OmpRpcEvent, { type: "extension_ui_request" }>,
101
- { method: "select" | "confirm" | "input" | "editor" }
102
- >;
103
95
  type Emit = (event: ProviderEvent) => void;
104
96
  const LOCAL_ONLY_SETTLE_MS = 5_000;
105
97
  const AGENT_END_STATE_TIMEOUT_MS = 2_000;
@@ -115,8 +107,8 @@ const MAX_AGENT_END_CORRELATION_MESSAGES = 512;
115
107
  const MAX_TRACKED_ENTRY_IDS = 1_024;
116
108
  const MAX_UNCLAIMED_BRANCH_ENTRIES = 1_024;
117
109
  const MAX_PENDING_USERS = 256;
118
- const MAX_PENDING_PERMISSIONS = 32;
119
- const MAX_PENDING_PERMISSION_BYTES = 2 * 1024 * 1024;
110
+ const _MAX_PENDING_PERMISSIONS = 32;
111
+ const _MAX_PENDING_PERMISSION_BYTES = 2 * 1024 * 1024;
120
112
  const MAX_USER_ECHOES = 512;
121
113
  const MAX_BUFFERED_TURN_EVENTS = 512;
122
114
  const MAX_BUFFERED_VALUE_ITEMS = 1_024;
@@ -128,8 +120,8 @@ const MAX_UNCLAIMED_BRANCH_BYTES = 4 * 1024 * 1024;
128
120
  class OmpCatalogEscape extends OmpPublicError {}
129
121
  const MAX_REPLAY_MESSAGES = 100_000;
130
122
  const REPLAY_TIMEOUT_MS = 20_000;
131
- const OMP_ASK_USER_FREEFORM_SENTINEL = "✏️ Type custom response...";
132
- const MAX_FREEFORM_RESPONSE_BYTES = 64 * 1024;
123
+ const _OMP_ASK_USER_FREEFORM_SENTINEL = "✏️ Type custom response...";
124
+ const _MAX_FREEFORM_RESPONSE_BYTES = 64 * 1024;
133
125
  const OMP_BUILTIN_COMMANDS: readonly OmpAvailableCommand[] = [
134
126
  {
135
127
  name: "compact",
@@ -255,56 +247,6 @@ type PendingAbort = {
255
247
  forceTerminal: boolean;
256
248
  promise: Promise<void>;
257
249
  };
258
- type PendingPermission = {
259
- nativeId: string;
260
- header: string;
261
- fingerprint: string;
262
- optionValues: ReadonlyMap<string, string>;
263
- actionBehaviors: ReadonlyMap<string, "allow" | "deny">;
264
- retainedBytes: number;
265
- displayValues: ReadonlyMap<string, string>;
266
- generation: number;
267
- runtime: OmpRuntimeSession;
268
- expiresAt?: number;
269
- timer?: unknown;
270
- turnId?: string;
271
- request: OmpQuestionRequest;
272
- freeformSentinel?: string;
273
- };
274
- type PendingToolPermission = {
275
- nativeId: string;
276
- toolCallId: string;
277
- fingerprint: string;
278
- retainedBytes: number;
279
- generation: number;
280
- runtime: OmpRuntimeSession;
281
- expiresAt?: number;
282
- timer?: unknown;
283
- turnId?: string;
284
- };
285
-
286
- type PendingFreeformSelection = {
287
- value: string;
288
- nativeSelectId: string;
289
- generation: number;
290
- runtime: OmpRuntimeSession;
291
- turnId?: string;
292
- };
293
-
294
- function permissionFingerprint(request: OmpQuestionRequest): string {
295
- return createHash("sha256").update(JSON.stringify(request)).digest("base64url");
296
- }
297
-
298
- type ActiveCompaction = {
299
- id: string;
300
- trigger: "auto" | "manual";
301
- turnId: string;
302
- generation: number;
303
- retrying: boolean;
304
- action?: string;
305
- preTokens?: number;
306
- };
307
-
308
250
  function providerError(error: unknown, fallback: string): { message: string } {
309
251
  return { message: isOmpPublicError(error) ? error.message : fallback };
310
252
  }
@@ -364,30 +306,12 @@ export class OmpProviderSession {
364
306
  private configRevision = 0;
365
307
  private recoveryUsesNativeConfig = false;
366
308
  private activeAbort: PendingAbort | null = null;
367
- private usageEpoch = 0;
368
- private usageSequence = 0;
369
- private usageSample: {
370
- turn: ActiveTurn;
371
- generation: number;
372
- runtime: OmpRuntimeSession;
373
- epoch: number;
374
- sequence: number;
375
- promise: Promise<OmpSessionState | undefined>;
376
- } | null = null;
377
- private activeCompaction: ActiveCompaction | null = null;
378
- private discardedCompactionEnds = 0;
379
- private lastUsage: ProviderUsage | null = null;
309
+ private readonly usage: OmpSessionUsage;
310
+ private readonly compaction: OmpSessionCompaction;
380
311
  private revertInFlight = false;
381
312
  private runtimeTurnCompleted = false;
382
313
  private commandCatalog: OmpAvailableCommand[];
383
- private permissionSequence = 0;
384
- private readonly permissionNamespace = randomUUID();
385
- private readonly pendingPermissions = new Map<string, PendingPermission>();
386
- private readonly inFlightPermissions = new Map<string, PendingPermission>();
387
- private pendingFreeformSelection: PendingFreeformSelection | null = null;
388
- private readonly pendingToolPermissions = new Map<string, PendingToolPermission>();
389
- private readonly inFlightToolPermissions = new Map<string, PendingToolPermission>();
390
- private readonly resolvedToolApprovalIds = new BoundedStringSet(MAX_TRACKED_ENTRY_IDS);
314
+ private readonly permissions: OmpSessionPermissions;
391
315
  private unsupportedThinkingNoticePending = false;
392
316
  private unsupportedThinkingNoticePublished = false;
393
317
 
@@ -420,6 +344,26 @@ export class OmpProviderSession {
420
344
  this.id = id;
421
345
  this.cwd = config.cwd;
422
346
  this.scheduler = scheduler;
347
+ this.usage = new OmpSessionUsage(
348
+ id,
349
+ emit,
350
+ scheduler,
351
+ () => ({
352
+ closed: this.closed,
353
+ runtimeDead: this.runtimeDead !== null,
354
+ generation: this.generation,
355
+ runtime: this.runtime,
356
+ activeTurn: this.activeTurn,
357
+ }),
358
+ (turn) =>
359
+ !this.closed &&
360
+ !this.runtimeDead &&
361
+ !turn.terminal &&
362
+ !turn.terminalizing &&
363
+ !turn.agentEndPending &&
364
+ turn.generation === this.generation &&
365
+ this.activeTurn === turn,
366
+ );
423
367
  this.dataFilter = new OmpPublicDataSerializer(outputRedactionValues);
424
368
  this.nativeModelsByPublicId = nativeModelsByPublicId;
425
369
  this.commandCatalog = commandCatalog;
@@ -433,6 +377,31 @@ export class OmpProviderSession {
433
377
  capabilities.includes("timeline.plugin"),
434
378
  hostTools.labels,
435
379
  );
380
+ this.compaction = new OmpSessionCompaction(
381
+ id,
382
+ emit,
383
+ this.projector,
384
+ this.dataFilter,
385
+ this.usage,
386
+ );
387
+ this.permissions = new OmpSessionPermissions({
388
+ sessionId: id,
389
+ emit,
390
+ scheduler,
391
+ dataFilter: this.dataFilter,
392
+ projector: this.projector,
393
+ readContext: () => ({
394
+ runtime: this.runtime,
395
+ generation: this.generation,
396
+ activeTurn: this.activeTurn,
397
+ closed: this.closed,
398
+ runtimeDead: this.runtimeDead !== null,
399
+ }),
400
+ handleRuntimeFailure: (message) => this.handleRuntimeFailure(message),
401
+ invalidateRuntime: (message) => this.invalidateRuntime(message),
402
+ markAgentEvidence: (turn) => this.markAgentEvidence(turn),
403
+ reevaluateDeferredTerminal: () => this.reevaluateDeferredPermissionTerminal(),
404
+ });
436
405
  this.subsessions = capabilities.includes("session.subsession")
437
406
  ? new OmpSubsessionProjector(
438
407
  id,
@@ -774,289 +743,6 @@ export class OmpProviderSession {
774
743
  });
775
744
  }
776
745
 
777
- private usageFrom(
778
- state: OmpSessionState | undefined,
779
- stats: OmpSessionStats | undefined,
780
- ): ProviderUsage | undefined {
781
- if (!state?.contextUsage && !stats) return undefined;
782
- const stateContext = state?.contextUsage;
783
- const statsContext = stats?.contextUsage;
784
- const modelCapacity = state?.model?.contextWindow;
785
- const inputTokens = stats?.tokens?.input;
786
- const cachedInputTokens = stats?.tokens?.cacheRead;
787
- const outputTokens = stats?.tokens?.output;
788
- const totalCostUsd = stats?.cost;
789
- const contextTokens =
790
- typeof stateContext?.tokens === "number"
791
- ? stateContext.tokens
792
- : typeof statsContext?.tokens === "number"
793
- ? statsContext.tokens
794
- : undefined;
795
- const contextWindow =
796
- typeof stateContext?.contextWindow === "number" && stateContext.contextWindow > 0
797
- ? stateContext.contextWindow
798
- : typeof statsContext?.contextWindow === "number" && statsContext.contextWindow > 0
799
- ? statsContext.contextWindow
800
- : typeof modelCapacity === "number" && modelCapacity > 0
801
- ? modelCapacity
802
- : undefined;
803
- const usage: ProviderUsage = {
804
- ...(typeof inputTokens === "number" ? { inputTokens } : {}),
805
- ...(typeof cachedInputTokens === "number" ? { cachedInputTokens } : {}),
806
- ...(typeof outputTokens === "number" ? { outputTokens } : {}),
807
- ...(typeof totalCostUsd === "number" ? { totalCostUsd } : {}),
808
- ...(contextTokens !== undefined ? { contextWindowUsedTokens: contextTokens } : {}),
809
- ...(contextWindow !== undefined ? { contextWindowMaxTokens: contextWindow } : {}),
810
- };
811
- return Object.keys(usage).length > 0 ? usage : undefined;
812
- }
813
-
814
- private ownsUsageSample(
815
- turn: ActiveTurn,
816
- generation: number,
817
- runtime: OmpRuntimeSession,
818
- ): boolean {
819
- return (
820
- !this.closed &&
821
- !this.runtimeDead &&
822
- !turn.terminal &&
823
- generation === this.generation &&
824
- runtime === this.runtime &&
825
- this.activeTurn === turn
826
- );
827
- }
828
-
829
- private publishUsageSnapshot(
830
- turn: ActiveTurn,
831
- minimumEpoch = this.usageEpoch,
832
- minimumSequence = turn.usageSampleFloor,
833
- ): Promise<OmpSessionState | undefined> {
834
- const generation = turn.generation;
835
- const runtime = this.runtime;
836
- if (!this.ownsUsageSample(turn, generation, runtime)) return Promise.resolve(undefined);
837
- const current = this.usageSample;
838
- if (current) {
839
- if (
840
- current.turn === turn &&
841
- current.generation === generation &&
842
- current.runtime === runtime &&
843
- current.epoch >= minimumEpoch &&
844
- current.sequence >= minimumSequence
845
- ) {
846
- return current.promise;
847
- }
848
- return current.promise.then(() => {
849
- if (!this.ownsUsageSample(turn, generation, runtime)) return undefined;
850
- return this.publishUsageSnapshot(turn, minimumEpoch, minimumSequence);
851
- });
852
- }
853
- const epoch = this.usageEpoch;
854
- const sequence = ++this.usageSequence;
855
- const promise = Promise.allSettled([runtime.getState(), runtime.getSessionStats()]).then(
856
- ([stateResult, statsResult]) => {
857
- if (
858
- !this.ownsUsageSample(turn, generation, runtime) ||
859
- epoch !== this.usageEpoch ||
860
- epoch < minimumEpoch ||
861
- sequence < turn.usageSampleFloor ||
862
- sequence < minimumSequence
863
- ) {
864
- return undefined;
865
- }
866
- const state = stateResult.status === "fulfilled" ? stateResult.value : undefined;
867
- const stats = statsResult.status === "fulfilled" ? statsResult.value : undefined;
868
- const usage = this.usageFrom(state, stats);
869
- if (usage) {
870
- this.lastUsage = usage;
871
- this.emit({ type: "session.usage", sessionId: this.id, turnId: turn.turnId, usage });
872
- }
873
- return state;
874
- },
875
- );
876
- this.usageSample = { turn, generation, runtime, epoch, sequence, promise };
877
- void promise.finally(() => {
878
- if (this.usageSample?.promise === promise) this.usageSample = null;
879
- });
880
- return promise;
881
- }
882
-
883
- private async boundedUsageSnapshot(
884
- turn: ActiveTurn,
885
- timeoutMs: number,
886
- ): Promise<OmpSessionState | undefined> {
887
- const timeout = Promise.withResolvers<undefined>();
888
- const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
889
- try {
890
- return await Promise.race([this.publishUsageSnapshot(turn), timeout.promise]);
891
- } finally {
892
- this.scheduler.clear(timer);
893
- }
894
- }
895
-
896
- private async boundedTerminalState(
897
- turn: ActiveTurn,
898
- timeoutMs: number,
899
- ): Promise<OmpSessionState | undefined> {
900
- const generation = turn.generation;
901
- const runtime = this.runtime;
902
- const timeout = Promise.withResolvers<undefined>();
903
- const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
904
- try {
905
- const state = await Promise.race([runtime.getState(), timeout.promise]);
906
- return this.ownsUsageSample(turn, generation, runtime) ? state : undefined;
907
- } catch {
908
- return undefined;
909
- } finally {
910
- this.scheduler.clear(timer);
911
- }
912
- }
913
-
914
- private isActiveTurn(turn: ActiveTurn): boolean {
915
- return (
916
- !this.closed &&
917
- !this.runtimeDead &&
918
- !turn.terminal &&
919
- !turn.terminalizing &&
920
- !turn.agentEndPending &&
921
- turn.generation === this.generation &&
922
- this.activeTurn === turn
923
- );
924
- }
925
-
926
- private scheduleUsagePoll(turn: ActiveTurn, delayMs = USAGE_POLL_MS): void {
927
- if (!this.isActiveTurn(turn) || turn.usagePollTimer !== undefined) return;
928
- turn.usagePollTimer = this.scheduler.set(() => {
929
- turn.usagePollTimer = undefined;
930
- this.pollUsage(turn);
931
- }, delayMs);
932
- }
933
-
934
- private pollUsage(turn: ActiveTurn): void {
935
- if (!this.isActiveTurn(turn) || turn.usagePoll) return;
936
- const poll = this.publishUsageSnapshot(turn).then(() => undefined);
937
- turn.usagePoll = poll;
938
- void poll.finally(() => {
939
- if (turn.usagePoll === poll) turn.usagePoll = undefined;
940
- this.scheduleUsagePoll(turn);
941
- });
942
- }
943
-
944
- private stopUsagePoll(turn: ActiveTurn): void {
945
- if (turn.usagePollTimer === undefined) return;
946
- this.scheduler.clear(turn.usagePollTimer);
947
- turn.usagePollTimer = undefined;
948
- }
949
-
950
- private startCompaction(turn: ActiveTurn, trigger: "auto" | "manual", action?: string): void {
951
- if (this.discardedCompactionEnds > 0) {
952
- this.discardedCompactionEnds += 1;
953
- return;
954
- }
955
- const active = this.activeCompaction;
956
- if (active && active.trigger === trigger && active.action === action) {
957
- active.retrying = false;
958
- return;
959
- }
960
- if (active) {
961
- this.retireCompaction("OMP emitted overlapping compactions");
962
- this.discardedCompactionEnds = 2;
963
- return;
964
- }
965
- const operation: ActiveCompaction = {
966
- id: randomUUID(),
967
- trigger,
968
- turnId: turn.turnId,
969
- generation: turn.generation,
970
- action,
971
- retrying: false,
972
- preTokens: this.lastUsage?.contextWindowUsedTokens,
973
- };
974
- this.activeCompaction = operation;
975
- this.projector.flush(true);
976
- this.emit({
977
- type: "timeline.item",
978
- sessionId: this.id,
979
- item: {
980
- id: operation.id,
981
- type: "compaction",
982
- status: "loading",
983
- trigger,
984
- ...(operation.preTokens !== undefined ? { preTokens: operation.preTokens } : {}),
985
- },
986
- });
987
- }
988
-
989
- private finishCompaction(
990
- state: "completed" | "failed" | "canceled" | "skipped",
991
- options: { tokensBefore?: number | null; message?: string } = {},
992
- ): void {
993
- const operation = this.activeCompaction;
994
- if (!operation) return;
995
- this.activeCompaction = null;
996
- this.projector.flush(true);
997
- if (state === "completed") {
998
- this.usageEpoch += 1;
999
- const staleSample = this.usageSample;
1000
- if (staleSample && staleSample.epoch < this.usageEpoch) {
1001
- this.usageSample = null;
1002
- staleSample.turn.usagePoll = undefined;
1003
- }
1004
- this.lastUsage = null;
1005
- }
1006
- if (state !== "completed") {
1007
- const defaultMessage =
1008
- state === "failed"
1009
- ? "OMP compaction failed"
1010
- : state === "canceled"
1011
- ? "OMP compaction canceled"
1012
- : "OMP compaction skipped";
1013
- this.emit({
1014
- type: "timeline.item",
1015
- sessionId: this.id,
1016
- item: {
1017
- id: operation.id,
1018
- type: "notification",
1019
- level: state === "failed" ? "error" : "info",
1020
- message: this.dataFilter.text(options.message ?? defaultMessage, 4_096),
1021
- },
1022
- });
1023
- return;
1024
- }
1025
- const tokensBefore = options.tokensBefore ?? operation.preTokens;
1026
- this.emit({
1027
- type: "timeline.item",
1028
- sessionId: this.id,
1029
- item: {
1030
- id: operation.id,
1031
- type: "compaction",
1032
- status: "completed",
1033
- trigger: operation.trigger,
1034
- ...(tokensBefore !== undefined ? { preTokens: tokensBefore } : {}),
1035
- },
1036
- });
1037
- }
1038
-
1039
- private retireCompaction(message: string): void {
1040
- const operation = this.activeCompaction;
1041
- if (!operation) return;
1042
- this.activeCompaction = null;
1043
- this.emit({
1044
- type: "timeline.item",
1045
- sessionId: this.id,
1046
- item: {
1047
- id: operation.id,
1048
- type: "compaction",
1049
- status: "completed",
1050
- trigger: operation.trigger,
1051
- },
1052
- });
1053
- this.emit({
1054
- type: "timeline.item",
1055
- sessionId: this.id,
1056
- item: { id: `${operation.id}:error`, type: "error", message },
1057
- });
1058
- }
1059
-
1060
746
  private async replayHistory(preferPersistedTranscript = false): Promise<void> {
1061
747
  if (!this.runtime.canReplayHistory) {
1062
748
  throw new OmpPublicError("OMP session history cannot be replayed safely");
@@ -1294,7 +980,7 @@ export class OmpProviderSession {
1294
980
  const configRefresh = this.configRefreshInFlight;
1295
981
  this.cancelConfigRefreshRetry();
1296
982
  this.lifetime.abort(new Error(message));
1297
- this.resolveAllPermissions(true);
983
+ this.permissions.resolveAllPermissions(true);
1298
984
  this.subsessions?.close();
1299
985
  this.projector.close();
1300
986
  this.unsubscribe();
@@ -1449,8 +1135,8 @@ export class OmpProviderSession {
1449
1135
  turn.starting = false;
1450
1136
  turn.acknowledged = true;
1451
1137
  this.startTurn(turn, false);
1452
- this.startCompaction(turn, "manual");
1453
- this.pollUsage(turn);
1138
+ this.compaction.start(turn, "manual");
1139
+ this.usage.poll(turn);
1454
1140
  const bufferedEvents = turn.bufferedEvents.splice(0);
1455
1141
  turn.replayingBufferedEvents = true;
1456
1142
  for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
@@ -1541,8 +1227,8 @@ export class OmpProviderSession {
1541
1227
  else {
1542
1228
  turn.steerReady.resolve();
1543
1229
  turn.terminal = true;
1544
- this.finishCompaction("failed", { message: "OMP prompt failed" });
1545
- this.resolveTurnPermissions(turn.turnId);
1230
+ this.compaction.finish("failed", { message: "OMP prompt failed" });
1231
+ this.permissions.resolveTurnPermissions(turn.turnId);
1546
1232
  this.projector.finishTurn(turn.turnId);
1547
1233
  if (this.activeTurn === turn) this.activeTurn = null;
1548
1234
  }
@@ -1714,7 +1400,7 @@ export class OmpProviderSession {
1714
1400
  const result = await compaction;
1715
1401
  turn.manualCompactionPending = false;
1716
1402
  if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
1717
- this.finishCompaction("completed", { tokensBefore: result.tokensBefore });
1403
+ this.compaction.finish("completed", { tokensBefore: result.tokensBefore });
1718
1404
  await this.finishTurn(turn, turn.interrupted ? "canceled" : "completed");
1719
1405
  } catch (error) {
1720
1406
  if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
@@ -1722,7 +1408,7 @@ export class OmpProviderSession {
1722
1408
  const raw = providerError(error, "OMP compaction failed");
1723
1409
  const failure = { message: this.dataFilter.text(raw.message, 4_096) };
1724
1410
  const state = turn.interrupted ? "canceled" : "failed";
1725
- this.finishCompaction(state, { message: failure.message });
1411
+ this.compaction.finish(state, { message: failure.message });
1726
1412
  await this.finishTurn(turn, state, state === "failed" ? failure : undefined, true, true);
1727
1413
  }
1728
1414
  }
@@ -1743,7 +1429,7 @@ export class OmpProviderSession {
1743
1429
  }
1744
1430
  const forceTerminal = turn.awaitingPermissionEvidence;
1745
1431
  turn.awaitingPermissionEvidence = false;
1746
- if (forceTerminal) this.resolveTurnPermissions(turn.turnId);
1432
+ if (forceTerminal) this.permissions.resolveTurnPermissions(turn.turnId);
1747
1433
  const pending = this.activeAbort;
1748
1434
  if (turn.interrupted) {
1749
1435
  if (pending?.turn === turn) await this.settleInterrupt(input.requestId, pending);
@@ -2154,118 +1840,7 @@ export class OmpProviderSession {
2154
1840
  }
2155
1841
 
2156
1842
  async permission(input: SessionPermissionInput): Promise<void> {
2157
- const typed = this.pendingToolPermissions.get(input.permissionId);
2158
- if (typed) {
2159
- await this.respondToToolPermission(input, typed);
2160
- return;
2161
- }
2162
- const pending = this.pendingPermissions.get(input.permissionId);
2163
- if (!pending) throw new OmpPublicError("Unknown OMP permission request");
2164
- if (!this.permissionOwnerIsCurrent(pending)) {
2165
- this.pendingPermissions.delete(input.permissionId);
2166
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
2167
- this.emit({
2168
- type: "session.permission_resolved",
2169
- sessionId: this.id,
2170
- permissionId: input.permissionId,
2171
- });
2172
- throw new OmpPublicError("OMP permission request is no longer active");
2173
- }
2174
- const { generation, runtime } = pending;
2175
- this.pendingPermissions.delete(input.permissionId);
2176
- this.inFlightPermissions.set(input.permissionId, pending);
2177
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
2178
- try {
2179
- const freeformValue = this.freeformSelection(pending, input.response);
2180
- if (freeformValue !== undefined) {
2181
- this.pendingFreeformSelection = {
2182
- value: freeformValue,
2183
- nativeSelectId: pending.nativeId,
2184
- generation,
2185
- runtime,
2186
- ...(pending.turnId ? { turnId: pending.turnId } : {}),
2187
- };
2188
- }
2189
- const response = this.extensionUiResponse(pending, input.response);
2190
- await runtime.respondToExtensionUi(response);
2191
- } catch (error) {
2192
- if (this.pendingFreeformSelection?.nativeSelectId === pending.nativeId) {
2193
- this.pendingFreeformSelection = null;
2194
- }
2195
- if (this.inFlightPermissions.get(input.permissionId) !== pending) return;
2196
- this.inFlightPermissions.delete(input.permissionId);
2197
- if (
2198
- this.permissionOwnerIsCurrent(pending) &&
2199
- generation === this.generation &&
2200
- runtime === this.runtime
2201
- ) {
2202
- this.pendingPermissions.set(input.permissionId, pending);
2203
- this.armPermissionTimeout(input.permissionId, pending);
2204
- throw error;
2205
- }
2206
- return;
2207
- }
2208
- if (this.inFlightPermissions.get(input.permissionId) !== pending) return;
2209
- this.inFlightPermissions.delete(input.permissionId);
2210
- this.emit({
2211
- type: "session.permission_resolved",
2212
- sessionId: this.id,
2213
- permissionId: input.permissionId,
2214
- });
2215
- this.reevaluateDeferredPermissionTerminal();
2216
- }
2217
- private async respondToToolPermission(
2218
- input: SessionPermissionInput,
2219
- pending: PendingToolPermission,
2220
- ): Promise<void> {
2221
- if (!this.permissionOwnerIsCurrent(pending)) {
2222
- this.pendingToolPermissions.delete(input.permissionId);
2223
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
2224
- this.resolvedToolApprovalIds.add(pending.nativeId);
2225
- this.emit({
2226
- type: "session.permission_resolved",
2227
- sessionId: this.id,
2228
- permissionId: input.permissionId,
2229
- });
2230
- throw new OmpPublicError("OMP permission request is no longer active");
2231
- }
2232
- if (
2233
- input.response.selectedActionId !== undefined &&
2234
- input.response.selectedActionId !== input.response.behavior
2235
- ) {
2236
- throw new OmpPublicError("OMP permission action is invalid");
2237
- }
2238
- this.pendingToolPermissions.delete(input.permissionId);
2239
- this.inFlightToolPermissions.set(input.permissionId, pending);
2240
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
2241
- try {
2242
- await pending.runtime.respondToToolApproval({
2243
- type: "tool_approval_response",
2244
- id: pending.nativeId,
2245
- toolCallId: pending.toolCallId,
2246
- approved: input.response.behavior === "allow",
2247
- });
2248
- } catch (error) {
2249
- if (this.inFlightToolPermissions.get(input.permissionId) !== pending) return;
2250
- this.inFlightToolPermissions.delete(input.permissionId);
2251
- this.resolvedToolApprovalIds.add(pending.nativeId);
2252
- this.emit({
2253
- type: "session.permission_resolved",
2254
- sessionId: this.id,
2255
- permissionId: input.permissionId,
2256
- });
2257
- this.reevaluateDeferredPermissionTerminal();
2258
- throw error;
2259
- }
2260
- if (this.inFlightToolPermissions.get(input.permissionId) !== pending) return;
2261
- this.inFlightToolPermissions.delete(input.permissionId);
2262
- this.resolvedToolApprovalIds.add(pending.nativeId);
2263
- this.emit({
2264
- type: "session.permission_resolved",
2265
- sessionId: this.id,
2266
- permissionId: input.permissionId,
2267
- });
2268
- this.reevaluateDeferredPermissionTerminal();
1843
+ await this.permissions.respond(input);
2269
1844
  }
2270
1845
 
2271
1846
  close(input?: SessionCloseInput): Promise<void> {
@@ -2299,12 +1874,12 @@ export class OmpProviderSession {
2299
1874
  if (turn.started) await this.finishTurn(turn, "canceled", undefined, true, true);
2300
1875
  else {
2301
1876
  this.settleUnstartedTurn(turn);
2302
- this.finishCompaction("canceled");
1877
+ this.compaction.finish("canceled");
2303
1878
  }
2304
1879
  }
2305
1880
  this.imageMaterializer.clear();
2306
- this.finishCompaction("canceled");
2307
- this.resolveAllPermissions(true);
1881
+ this.compaction.finish("canceled");
1882
+ this.permissions.resolveAllPermissions(true);
2308
1883
  this.closed = true;
2309
1884
  this.configRefreshAttempts = 0;
2310
1885
  this.configRefreshDirty = false;
@@ -2463,7 +2038,7 @@ export class OmpProviderSession {
2463
2038
  }
2464
2039
  if (this.subsessions) await recovered.setSubagentSubscription("events");
2465
2040
  this.generation += 1;
2466
- this.lastUsage = null;
2041
+ this.usage.clearLatest();
2467
2042
  this.runtimeDead = null;
2468
2043
  this.runtimeDisposal = null;
2469
2044
  unsubscribeBootstrap();
@@ -2629,7 +2204,7 @@ export class OmpProviderSession {
2629
2204
  this.handleRuntimeFailure("OMP emitted an unnegotiated tool approval request");
2630
2205
  return;
2631
2206
  }
2632
- this.publishToolPermission(event);
2207
+ this.permissions.publishToolPermission(event);
2633
2208
  return;
2634
2209
  }
2635
2210
  if (event.type === "tool_approval_cancel") {
@@ -2637,18 +2212,17 @@ export class OmpProviderSession {
2637
2212
  this.handleRuntimeFailure("OMP emitted an unnegotiated tool approval cancellation");
2638
2213
  return;
2639
2214
  }
2640
- this.cancelToolPermission(event);
2215
+ this.permissions.cancelToolPermission(event);
2641
2216
  return;
2642
2217
  }
2643
2218
  if (event.type === "extension_ui_request") {
2644
2219
  if (event.method === "cancel") {
2645
- if (this.pendingFreeformSelection?.nativeSelectId === event.targetId) {
2646
- this.pendingFreeformSelection = null;
2647
- }
2648
- this.resolvePermissionByNativeId(event.targetId ?? "");
2220
+ this.permissions.clearPendingFreeformSelection(event.targetId);
2221
+ this.permissions.resolvePermissionByNativeId(event.targetId ?? "");
2649
2222
  return;
2650
2223
  }
2651
- if (event.method === "input" && this.submitPendingFreeformSelection(event)) return;
2224
+ if (event.method === "input" && this.permissions.submitPendingFreeformSelection(event))
2225
+ return;
2652
2226
  if (
2653
2227
  event.method === "select" ||
2654
2228
  event.method === "confirm" ||
@@ -2659,7 +2233,7 @@ export class OmpProviderSession {
2659
2233
  this.handleRuntimeFailure();
2660
2234
  return;
2661
2235
  }
2662
- this.publishPermission(event);
2236
+ this.permissions.publishPermission(event);
2663
2237
  return;
2664
2238
  }
2665
2239
 
@@ -2810,50 +2384,20 @@ export class OmpProviderSession {
2810
2384
  if (event.type === "auto_compaction_start") {
2811
2385
  turn.nativeActivity = true;
2812
2386
  this.cancelLocalOnlyCompletion(turn);
2813
- this.startCompaction(turn, "auto", event.action);
2387
+ this.compaction.start(turn, "auto", event.action);
2814
2388
  return;
2815
2389
  }
2816
2390
  if (event.type === "auto_compaction_end") {
2817
- if (this.discardedCompactionEnds > 0) {
2818
- this.discardedCompactionEnds -= 1;
2819
- return;
2391
+ if (this.compaction.handleAutoEnd(turn, event)) {
2392
+ this.usage.schedulePoll(turn, USAGE_REFRESH_MS);
2820
2393
  }
2821
- const operation = this.activeCompaction;
2822
- if (
2823
- operation?.trigger !== "auto" ||
2824
- operation.turnId !== turn.turnId ||
2825
- operation.generation !== turn.generation
2826
- ) {
2827
- return;
2828
- }
2829
- if (event.action !== undefined && operation.action !== event.action) {
2830
- this.retireCompaction("OMP emitted overlapping compactions");
2831
- this.discardedCompactionEnds = 1;
2832
- return;
2833
- }
2834
- if (event.willRetry) {
2835
- operation.retrying = true;
2836
- return;
2837
- }
2838
- const state = event.aborted
2839
- ? "canceled"
2840
- : event.errorMessage
2841
- ? "failed"
2842
- : event.skipped
2843
- ? "skipped"
2844
- : "completed";
2845
- this.finishCompaction(state, {
2846
- tokensBefore: event.result?.tokensBefore ?? event.result?.preTokens,
2847
- message: event.errorMessage,
2848
- });
2849
- this.scheduleUsagePoll(turn, USAGE_REFRESH_MS);
2850
2394
  return;
2851
2395
  }
2852
2396
  if (event.type === "agent_end" && turn.manualCompactionPending) return;
2853
2397
  if (event.type === "tool_execution_start") turn.activeToolCallIds.add(event.toolCallId);
2854
2398
  if (event.type === "tool_execution_end") {
2855
2399
  turn.activeToolCallIds.delete(event.toolCallId);
2856
- if (event.toolName === "ask_user") this.pendingFreeformSelection = null;
2400
+ if (event.toolName === "ask_user") this.permissions.clearPendingFreeformSelection();
2857
2401
  }
2858
2402
  if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
2859
2403
  try {
@@ -3128,24 +2672,6 @@ export class OmpProviderSession {
3128
2672
  });
3129
2673
  }
3130
2674
 
3131
- private submitPendingFreeformSelection(
3132
- request: Extract<OmpQuestionRequest, { method: "input" }>,
3133
- ): boolean {
3134
- const pending = this.pendingFreeformSelection;
3135
- if (!pending) return false;
3136
- this.pendingFreeformSelection = null;
3137
- if (
3138
- pending.generation !== this.generation ||
3139
- pending.runtime !== this.runtime ||
3140
- (pending.turnId !== undefined && this.activeTurn?.turnId !== pending.turnId)
3141
- ) {
3142
- return false;
3143
- }
3144
- void pending.runtime
3145
- .respondToExtensionUi({ type: "extension_ui_response", id: request.id, value: pending.value })
3146
- .catch(() => this.handleRuntimeFailure("OMP freeform response failed"));
3147
- return true;
3148
- }
3149
2675
  private replaceSlashCommands(commands: OmpAvailableCommand[]): void {
3150
2676
  this.slashCommands.clear();
3151
2677
  for (const command of [...OMP_BUILTIN_COMMANDS, ...commands]) {
@@ -3177,687 +2703,11 @@ export class OmpProviderSession {
3177
2703
  }),
3178
2704
  });
3179
2705
  }
3180
- private toolPermissionDetail(
3181
- request: OmpToolApprovalRequest,
3182
- ): ProviderToolCallDetail | undefined {
3183
- switch (request.identity.kind) {
3184
- case "shell":
3185
- return {
3186
- type: "shell",
3187
- command: this.dataFilter.text(request.identity.command, 24 * 1024),
3188
- };
3189
- case "edit":
3190
- return {
3191
- type: "edit",
3192
- filePath: this.dataFilter.text(request.identity.paths[0] ?? "", 4_096),
3193
- newString: this.dataFilter.text(request.identity.content, 20 * 1024),
3194
- };
3195
- case "write":
3196
- return {
3197
- type: "write",
3198
- filePath: this.dataFilter.text(request.identity.path, 4_096),
3199
- content: this.dataFilter.text(request.identity.content, 20 * 1024),
3200
- };
3201
- case "other":
3202
- return;
3203
- }
3204
- }
3205
-
3206
- private publishToolPermission(request: OmpToolApprovalRequest): void {
3207
- const fingerprint = createHash("sha256").update(JSON.stringify(request)).digest("base64url");
3208
- if (this.resolvedToolApprovalIds.has(request.id)) {
3209
- this.handleRuntimeFailure("OMP reused a resolved tool approval identifier");
3210
- return;
3211
- }
3212
- for (const pending of [
3213
- ...this.pendingToolPermissions.values(),
3214
- ...this.inFlightToolPermissions.values(),
3215
- ]) {
3216
- if (pending.nativeId !== request.id) continue;
3217
- if (pending.fingerprint !== fingerprint || pending.toolCallId !== request.toolCallId) {
3218
- this.handleRuntimeFailure("OMP changed a pending tool approval request");
3219
- }
3220
- return;
3221
- }
3222
- const pendingCount =
3223
- this.pendingPermissions.size +
3224
- this.inFlightPermissions.size +
3225
- this.pendingToolPermissions.size +
3226
- this.inFlightToolPermissions.size;
3227
- if (pendingCount >= MAX_PENDING_PERMISSIONS) {
3228
- this.resolvedToolApprovalIds.add(request.id);
3229
- void this.runtime
3230
- .respondToToolApproval({
3231
- type: "tool_approval_response",
3232
- id: request.id,
3233
- toolCallId: request.toolCallId,
3234
- cancelled: true,
3235
- })
3236
- .catch(() => this.handleRuntimeFailure());
3237
- return;
3238
- }
3239
-
3240
- this.permissionSequence += 1;
3241
- const permissionId = `omp:permission:${this.permissionNamespace}:${this.permissionSequence}`;
3242
- const detail = this.toolPermissionDetail(request);
3243
- const filteredInput = this.dataFilter.json(request.input, 8 * 1024, 32 * 1024);
3244
- const input =
3245
- filteredInput && typeof filteredInput === "object" && !Array.isArray(filteredInput)
3246
- ? filteredInput
3247
- : {};
3248
- const descriptionParts = [
3249
- request.detail.reason,
3250
- ...request.detail.lines,
3251
- ...(request.detail.providerSafetyChecks ?? []),
3252
- ].filter((part): part is string => Boolean(part));
3253
- const publicRequest = {
3254
- id: permissionId,
3255
- name: `omp.${this.dataFilter.text(request.toolName, 256)}`,
3256
- kind: "tool" as const,
3257
- title: `Allow ${this.dataFilter.text(request.toolName, 256)}?`,
3258
- ...(descriptionParts.length > 0
3259
- ? { description: this.dataFilter.text(descriptionParts.join("\n"), 16 * 1024) }
3260
- : {}),
3261
- input: {
3262
- ...input,
3263
- identity: this.dataFilter.json(request.identity, 20 * 1024, 32 * 1024),
3264
- tier: request.tier,
3265
- toolCallId: request.toolCallId,
3266
- },
3267
- ...(detail ? { detail } : {}),
3268
- actions: [
3269
- { id: "allow", label: "Allow", behavior: "allow" as const, variant: "primary" as const },
3270
- { id: "deny", label: "Deny", behavior: "deny" as const, variant: "danger" as const },
3271
- ],
3272
- metadata: {
3273
- tier: request.tier,
3274
- redacted: request.detail.redacted,
3275
- truncated: request.detail.truncated,
3276
- redactedFields: request.detail.redactedFields,
3277
- truncatedFields: request.detail.truncatedFields,
3278
- },
3279
- };
3280
- const pending: PendingToolPermission = {
3281
- nativeId: request.id,
3282
- toolCallId: request.toolCallId,
3283
- fingerprint,
3284
- retainedBytes: boundedJsonBytes(
3285
- publicRequest,
3286
- MAX_PENDING_PERMISSION_BYTES,
3287
- 512,
3288
- MAX_PENDING_PERMISSION_BYTES,
3289
- 4_096,
3290
- ),
3291
- generation: this.generation,
3292
- runtime: this.runtime,
3293
- ...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
3294
- ...(request.timeout !== undefined ? { expiresAt: Date.now() + request.timeout } : {}),
3295
- };
3296
- let retainedBytes = pending.retainedBytes;
3297
- for (const item of this.pendingPermissions.values()) retainedBytes += item.retainedBytes;
3298
- for (const item of this.inFlightPermissions.values()) retainedBytes += item.retainedBytes;
3299
- for (const item of this.pendingToolPermissions.values()) retainedBytes += item.retainedBytes;
3300
- for (const item of this.inFlightToolPermissions.values()) retainedBytes += item.retainedBytes;
3301
- if (
3302
- pending.retainedBytes === Number.POSITIVE_INFINITY ||
3303
- retainedBytes > MAX_PENDING_PERMISSION_BYTES
3304
- ) {
3305
- void this.runtime
3306
- .respondToToolApproval({
3307
- type: "tool_approval_response",
3308
- id: request.id,
3309
- toolCallId: request.toolCallId,
3310
- cancelled: true,
3311
- })
3312
- .catch(() => this.handleRuntimeFailure());
3313
- this.resolvedToolApprovalIds.add(request.id);
3314
- return;
3315
- }
3316
- this.pendingToolPermissions.set(permissionId, pending);
3317
- this.armToolPermissionTimeout(permissionId, pending);
3318
- this.projector.markAskPermissionRendered();
3319
- if (this.activeTurn) {
3320
- this.activeTurn.awaitingPermissionEvidence = true;
3321
- this.markAgentEvidence(this.activeTurn);
3322
- }
3323
- this.emit({ type: "session.permission", sessionId: this.id, request: publicRequest });
3324
- }
3325
-
3326
- private cancelToolPermission(request: OmpToolApprovalCancel): void {
3327
- for (const permissions of [this.pendingToolPermissions, this.inFlightToolPermissions]) {
3328
- for (const [permissionId, pending] of permissions) {
3329
- if (pending.nativeId !== request.targetId) continue;
3330
- if (pending.toolCallId !== request.toolCallId) {
3331
- this.handleRuntimeFailure("OMP tool approval cancellation did not match its tool call");
3332
- return;
3333
- }
3334
- permissions.delete(permissionId);
3335
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
3336
- this.resolvedToolApprovalIds.add(pending.nativeId);
3337
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
3338
- this.reevaluateDeferredPermissionTerminal();
3339
- return;
3340
- }
3341
- }
3342
- if (!this.resolvedToolApprovalIds.has(request.targetId)) {
3343
- this.handleRuntimeFailure("OMP canceled an unknown tool approval request");
3344
- }
3345
- }
3346
-
3347
- private publishPermission(request: OmpQuestionRequest): void {
3348
- if (request.method === "select" && !request.options?.length) {
3349
- this.handleRuntimeFailure();
3350
- return;
3351
- }
3352
- const fingerprint = permissionFingerprint(request);
3353
- let existingId: string | undefined;
3354
- let existingPending: PendingPermission | undefined;
3355
- for (const [permissionId, pending] of this.pendingPermissions) {
3356
- if (pending.nativeId !== request.id) continue;
3357
- if (pending.fingerprint === fingerprint) {
3358
- existingId = permissionId;
3359
- existingPending = pending;
3360
- } else {
3361
- this.pendingPermissions.delete(permissionId);
3362
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
3363
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
3364
- }
3365
- break;
3366
- }
3367
- if (!existingId) {
3368
- for (const pending of this.inFlightPermissions.values()) {
3369
- if (pending.nativeId !== request.id) continue;
3370
- if (pending.fingerprint !== fingerprint) this.handleRuntimeFailure();
3371
- return;
3372
- }
3373
- }
3374
- if (
3375
- !existingId &&
3376
- this.pendingPermissions.size +
3377
- this.inFlightPermissions.size +
3378
- this.pendingToolPermissions.size +
3379
- this.inFlightToolPermissions.size >=
3380
- MAX_PENDING_PERMISSIONS
3381
- ) {
3382
- this.rejectPermissionRequest(request, "Too many OMP questions are already pending");
3383
- return;
3384
- }
3385
- if (existingPending?.timer !== undefined) this.scheduler.clear(existingPending.timer);
3386
- if (!existingId) this.permissionSequence += 1;
3387
- const id =
3388
- existingId ?? `omp:permission:${this.permissionNamespace}:${this.permissionSequence}`;
3389
- const header = this.dataFilter.text(request.title ?? "OMP question", 4_096);
3390
- const freeformSentinel =
3391
- request.method === "select" && request.options.includes(OMP_ASK_USER_FREEFORM_SENTINEL)
3392
- ? OMP_ASK_USER_FREEFORM_SENTINEL
3393
- : undefined;
3394
- const optionValues = new Map<string, string>();
3395
- const displayValues = new Map<string, string>();
3396
- const usedOptionLabels = new Set<string>();
3397
- const optionDetails = request.method === "select" ? request.optionDetails : undefined;
3398
- const selectableOptions =
3399
- request.method === "select"
3400
- ? request.options.flatMap((nativeValue, index) =>
3401
- nativeValue === freeformSentinel ? [] : [{ nativeValue, index }],
3402
- )
3403
- : [];
3404
- const options =
3405
- request.method === "select"
3406
- ? selectableOptions.map(({ nativeValue, index }) => {
3407
- const baseLabel = this.dataFilter.text(nativeValue, 4_096);
3408
- let label = baseLabel;
3409
- let suffix = 2;
3410
- while (usedOptionLabels.has(label)) {
3411
- label = `${baseLabel} (${suffix})`;
3412
- suffix += 1;
3413
- }
3414
- usedOptionLabels.add(label);
3415
- const value = `${id}:option:${index}`;
3416
- optionValues.set(value, nativeValue);
3417
- displayValues.set(label, nativeValue);
3418
- return {
3419
- label,
3420
- value,
3421
- ...(optionDetails?.[index]?.description
3422
- ? {
3423
- description: this.dataFilter.text(
3424
- optionDetails[index]?.description ?? "",
3425
- 16_384,
3426
- ),
3427
- }
3428
- : {}),
3429
- };
3430
- })
3431
- : undefined;
3432
- const questionOptions =
3433
- request.method === "confirm" ? [{ label: "Yes" }, { label: "No" }] : (options ?? []);
3434
- const actions =
3435
- request.method === "select"
3436
- ? [
3437
- ...(options ?? []).map((option) => ({
3438
- id: option.value,
3439
- label: option.label,
3440
- behavior: "allow" as const,
3441
- variant: "secondary" as const,
3442
- })),
3443
- {
3444
- id: "cancel",
3445
- label: "Cancel",
3446
- behavior: "deny" as const,
3447
- variant: "secondary" as const,
3448
- },
3449
- ]
3450
- : [
3451
- {
3452
- id: "submit",
3453
- label: request.method === "confirm" ? "Confirm" : "Submit",
3454
- behavior: "allow" as const,
3455
- variant: "primary" as const,
3456
- },
3457
- {
3458
- id: "cancel",
3459
- label: "Cancel",
3460
- behavior: "deny" as const,
3461
- variant: "secondary" as const,
3462
- },
3463
- ];
3464
- const pending: PendingPermission = {
3465
- nativeId: request.id,
3466
- header,
3467
- fingerprint,
3468
- optionValues,
3469
- actionBehaviors: new Map(actions.map((action) => [action.id, action.behavior])),
3470
- displayValues,
3471
- generation: this.generation,
3472
- runtime: this.runtime,
3473
- request,
3474
- retainedBytes: boundedJsonBytes(
3475
- { request, header, options: questionOptions, actions },
3476
- MAX_PENDING_PERMISSION_BYTES,
3477
- 512,
3478
- MAX_PENDING_PERMISSION_BYTES,
3479
- 4_096,
3480
- ),
3481
- ...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
3482
- ...(request.timeout !== undefined ? { expiresAt: Date.now() + request.timeout } : {}),
3483
- ...(freeformSentinel ? { freeformSentinel } : {}),
3484
- };
3485
- let retainedPermissionBytes = pending.retainedBytes;
3486
- for (const [permissionId, retained] of this.pendingPermissions) {
3487
- if (permissionId !== existingId) retainedPermissionBytes += retained.retainedBytes;
3488
- }
3489
- for (const retained of this.inFlightPermissions.values()) {
3490
- retainedPermissionBytes += retained.retainedBytes;
3491
- }
3492
- for (const retained of this.pendingToolPermissions.values()) {
3493
- retainedPermissionBytes += retained.retainedBytes;
3494
- }
3495
- for (const retained of this.inFlightToolPermissions.values()) {
3496
- retainedPermissionBytes += retained.retainedBytes;
3497
- }
3498
- if (
3499
- pending.retainedBytes === Number.POSITIVE_INFINITY ||
3500
- retainedPermissionBytes > MAX_PENDING_PERMISSION_BYTES
3501
- ) {
3502
- if (existingId) {
3503
- this.pendingPermissions.delete(existingId);
3504
- this.emit({
3505
- type: "session.permission_resolved",
3506
- sessionId: this.id,
3507
- permissionId: existingId,
3508
- });
3509
- }
3510
- this.rejectPermissionRequest(request, "OMP question data exceeded the pending input budget");
3511
- return;
3512
- }
3513
- this.pendingPermissions.set(id, pending);
3514
- this.armPermissionTimeout(id, pending);
3515
- this.projector.markAskPermissionRendered();
3516
- if (this.activeTurn) {
3517
- this.activeTurn.awaitingPermissionEvidence = true;
3518
- this.markAgentEvidence(this.activeTurn);
3519
- }
3520
- this.emit({
3521
- type: "session.permission",
3522
- sessionId: this.id,
3523
- request: {
3524
- id,
3525
- name: `omp.${request.method}`,
3526
- kind: "question",
3527
- title: header,
3528
- ...(request.method === "confirm"
3529
- ? { description: this.dataFilter.text(request.message, 64 * 1024) }
3530
- : {}),
3531
- input: {
3532
- questions: [
3533
- {
3534
- header,
3535
- question: this.dataFilter.text(
3536
- request.method === "confirm" ? request.message : request.title,
3537
- 64 * 1024,
3538
- ),
3539
- options: questionOptions,
3540
- multiSelect: false,
3541
- ...(freeformSentinel ? { allowOther: true } : {}),
3542
- ...(request.method === "input" && request.placeholder
3543
- ? { placeholder: this.dataFilter.text(request.placeholder, 4_096) }
3544
- : {}),
3545
- ...((request.method === "input" || request.method === "editor") && request.prefill
3546
- ? { prefill: this.dataFilter.text(request.prefill) }
3547
- : {}),
3548
- },
3549
- ],
3550
- },
3551
- actions,
3552
- },
3553
- });
3554
- }
3555
-
3556
- private rejectPermissionRequest(request: OmpQuestionRequest, description: string): void {
3557
- this.emit({
3558
- type: "session.notice",
3559
- sessionId: this.id,
3560
- notice: {
3561
- id: `omp:permission-rejected:${this.permissionSequence + 1}`,
3562
- severity: "warning",
3563
- title: "OMP question canceled",
3564
- description,
3565
- },
3566
- });
3567
- void this.runtime
3568
- .respondToExtensionUi({ type: "extension_ui_response", id: request.id, cancelled: true })
3569
- .catch(() => this.handleRuntimeFailure());
3570
- }
3571
-
3572
- private freeformSelection(
3573
- pending: PendingPermission,
3574
- response: ProviderPermissionResponse,
3575
- ): string | undefined {
3576
- if (
3577
- pending.request.method !== "select" ||
3578
- !pending.freeformSentinel ||
3579
- response.behavior !== "allow" ||
3580
- response.selectedActionId !== undefined
3581
- ) {
3582
- return;
3583
- }
3584
- const answers = response.updatedInput?.answers;
3585
- const answer =
3586
- answers && typeof answers === "object" && !Array.isArray(answers)
3587
- ? answers[pending.header]
3588
- : undefined;
3589
- const value = Array.isArray(answer) ? answer[0] : answer;
3590
- if (
3591
- typeof value !== "string" ||
3592
- pending.optionValues.has(value) ||
3593
- pending.displayValues.has(value)
3594
- ) {
3595
- return;
3596
- }
3597
- if (
3598
- value === pending.freeformSentinel ||
3599
- value.trim().length === 0 ||
3600
- value.includes("\0") ||
3601
- utf8Bytes(value) > MAX_FREEFORM_RESPONSE_BYTES
3602
- ) {
3603
- throw new OmpPublicError("OMP freeform response is invalid");
3604
- }
3605
- return value;
3606
- }
3607
-
3608
- private extensionUiResponse(
3609
- pending: PendingPermission,
3610
- response: ProviderPermissionResponse,
3611
- ): OmpExtensionUiResponse {
3612
- if (response.selectedActionId !== undefined) {
3613
- const expectedBehavior = pending.actionBehaviors.get(response.selectedActionId);
3614
- if (expectedBehavior === undefined || expectedBehavior !== response.behavior) {
3615
- throw new OmpPublicError("OMP permission action is invalid");
3616
- }
3617
- }
3618
- const { nativeId, request, header } = pending;
3619
- if (request.method === "confirm") {
3620
- if (response.behavior === "deny") {
3621
- return { type: "extension_ui_response", id: nativeId, confirmed: false };
3622
- }
3623
- const answers = response.updatedInput?.answers;
3624
- const answer =
3625
- answers && typeof answers === "object" && !Array.isArray(answers)
3626
- ? answers[header]
3627
- : undefined;
3628
- return {
3629
- type: "extension_ui_response",
3630
- id: nativeId,
3631
- confirmed: typeof answer === "string" ? /^yes$/iu.test(answer.trim()) : true,
3632
- };
3633
- }
3634
- this.reevaluateDeferredPermissionTerminal();
3635
- if (response.behavior === "deny") {
3636
- return { type: "extension_ui_response", id: nativeId, cancelled: true };
3637
- }
3638
- const selectedValue = response.selectedActionId
3639
- ? pending.optionValues.get(response.selectedActionId)
3640
- : undefined;
3641
- if (selectedValue !== undefined) {
3642
- return { type: "extension_ui_response", id: nativeId, value: selectedValue };
3643
- }
3644
- const answers = response.updatedInput?.answers;
3645
- if (!answers || typeof answers !== "object" || Array.isArray(answers)) {
3646
- throw new OmpPublicError("OMP question response requires answers");
3647
- }
3648
- const answer = answers[header];
3649
- const publicValue = Array.isArray(answer) ? answer[0] : answer;
3650
- if (typeof publicValue !== "string") {
3651
- throw new OmpPublicError("OMP question response is invalid");
3652
- }
3653
- if (
3654
- request.method === "select" &&
3655
- pending.freeformSentinel &&
3656
- publicValue !== pending.freeformSentinel &&
3657
- !pending.displayValues.has(publicValue) &&
3658
- !pending.optionValues.has(publicValue)
3659
- ) {
3660
- if (
3661
- publicValue.trim().length === 0 ||
3662
- publicValue.includes("\0") ||
3663
- utf8Bytes(publicValue) > MAX_FREEFORM_RESPONSE_BYTES
3664
- ) {
3665
- throw new OmpPublicError("OMP freeform response is invalid");
3666
- }
3667
- this.pendingFreeformSelection = {
3668
- value: publicValue,
3669
- nativeSelectId: pending.nativeId,
3670
- generation: pending.generation,
3671
- runtime: pending.runtime,
3672
- ...(pending.turnId ? { turnId: pending.turnId } : {}),
3673
- };
3674
- return {
3675
- type: "extension_ui_response",
3676
- id: nativeId,
3677
- value: pending.freeformSentinel,
3678
- };
3679
- }
3680
- if (request.method === "select") {
3681
- const mapped =
3682
- pending.optionValues.get(publicValue) ?? pending.displayValues.get(publicValue);
3683
- if (mapped === undefined) {
3684
- throw new OmpPublicError("OMP selection response is invalid");
3685
- }
3686
- return { type: "extension_ui_response", id: nativeId, value: mapped };
3687
- }
3688
- return { type: "extension_ui_response", id: nativeId, value: publicValue };
3689
- }
3690
-
3691
- private resolvePermissionByNativeId(nativeId: string): void {
3692
- for (const permissions of [this.pendingPermissions, this.inFlightPermissions]) {
3693
- for (const [permissionId, pending] of permissions) {
3694
- if (pending.nativeId !== nativeId) continue;
3695
- permissions.delete(permissionId);
3696
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
3697
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
3698
- this.reevaluateDeferredPermissionTerminal();
3699
- return;
3700
- }
3701
- }
3702
- }
3703
-
3704
- private resolveTurnPermissions(turnId: string): void {
3705
- if (this.pendingFreeformSelection?.turnId === turnId) this.pendingFreeformSelection = null;
3706
- this.resolvePermissions((pending) => pending.turnId === turnId, true);
3707
- this.resolveToolPermissions((pending) => pending.turnId === turnId, true);
3708
- }
3709
- private resolveAllPermissions(cancelNative = false): void {
3710
- this.pendingFreeformSelection = null;
3711
- this.resolvePermissions(() => true, cancelNative);
3712
- this.resolveToolPermissions(() => true, cancelNative);
3713
- }
3714
-
3715
- private resolvePermissions(
3716
- matches: (pending: PendingPermission) => boolean,
3717
- cancelNative: boolean,
3718
- ): void {
3719
- const permissionIds = new Set<string>();
3720
- for (const permissions of [this.pendingPermissions, this.inFlightPermissions]) {
3721
- for (const [permissionId, pending] of permissions) {
3722
- if (!matches(pending)) continue;
3723
- permissions.delete(permissionId);
3724
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
3725
- permissionIds.add(permissionId);
3726
- if (cancelNative && permissions === this.pendingPermissions) {
3727
- void pending.runtime
3728
- .respondToExtensionUi({
3729
- type: "extension_ui_response",
3730
- id: pending.nativeId,
3731
- cancelled: true,
3732
- })
3733
- .catch(() => {
3734
- if (!this.closed && !this.runtimeDead) {
3735
- this.invalidateRuntime("OMP permission cancellation failed");
3736
- }
3737
- });
3738
- }
3739
- }
3740
- }
3741
- for (const permissionId of permissionIds) {
3742
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
3743
- }
3744
- this.reevaluateDeferredPermissionTerminal();
3745
- }
3746
- private resolveToolPermissions(
3747
- matches: (pending: PendingToolPermission) => boolean,
3748
- cancelNative: boolean,
3749
- ): void {
3750
- const permissionIds = new Set<string>();
3751
- for (const permissions of [this.pendingToolPermissions, this.inFlightToolPermissions]) {
3752
- for (const [permissionId, pending] of permissions) {
3753
- if (!matches(pending)) continue;
3754
- permissions.delete(permissionId);
3755
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
3756
- this.resolvedToolApprovalIds.add(pending.nativeId);
3757
- permissionIds.add(permissionId);
3758
- if (cancelNative && permissions === this.pendingToolPermissions) {
3759
- void pending.runtime
3760
- .respondToToolApproval({
3761
- type: "tool_approval_response",
3762
- id: pending.nativeId,
3763
- toolCallId: pending.toolCallId,
3764
- cancelled: true,
3765
- })
3766
- .catch(() => {
3767
- if (!this.closed && !this.runtimeDead) {
3768
- this.invalidateRuntime("OMP tool permission cancellation failed");
3769
- }
3770
- });
3771
- }
3772
- }
3773
- }
3774
- for (const permissionId of permissionIds) {
3775
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
3776
- }
3777
- this.reevaluateDeferredPermissionTerminal();
3778
- }
3779
-
3780
- private armToolPermissionTimeout(permissionId: string, pending: PendingToolPermission): void {
3781
- if (pending.expiresAt === undefined) return;
3782
- const remainingMs = Math.max(0, pending.expiresAt - Date.now());
3783
- pending.timer = this.scheduler.set(() => {
3784
- if (this.pendingToolPermissions.get(permissionId) !== pending) return;
3785
- this.pendingToolPermissions.delete(permissionId);
3786
- this.inFlightToolPermissions.set(permissionId, pending);
3787
- void pending.runtime
3788
- .respondToToolApproval({
3789
- type: "tool_approval_response",
3790
- id: pending.nativeId,
3791
- toolCallId: pending.toolCallId,
3792
- cancelled: true,
3793
- timedOut: true,
3794
- })
3795
- .then(
3796
- () => {
3797
- if (this.inFlightToolPermissions.get(permissionId) !== pending) return;
3798
- this.inFlightToolPermissions.delete(permissionId);
3799
- this.resolvedToolApprovalIds.add(pending.nativeId);
3800
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
3801
- this.reevaluateDeferredPermissionTerminal();
3802
- },
3803
- () => this.handleRuntimeFailure(),
3804
- );
3805
- }, remainingMs);
3806
- }
3807
-
3808
- private armPermissionTimeout(permissionId: string, pending: PendingPermission): void {
3809
- if (pending.expiresAt === undefined) return;
3810
- const remainingMs = Math.max(0, pending.expiresAt - Date.now());
3811
- pending.timer = this.scheduler.set(() => {
3812
- if (this.pendingPermissions.get(permissionId) !== pending) return;
3813
- this.pendingPermissions.delete(permissionId);
3814
- if (!this.permissionOwnerIsCurrent(pending)) {
3815
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
3816
- this.reevaluateDeferredPermissionTerminal();
3817
- return;
3818
- }
3819
- this.inFlightPermissions.set(permissionId, pending);
3820
- void pending.runtime
3821
- .respondToExtensionUi({
3822
- type: "extension_ui_response",
3823
- id: pending.nativeId,
3824
- cancelled: true,
3825
- timedOut: true,
3826
- })
3827
- .then(
3828
- () => {
3829
- if (this.inFlightPermissions.get(permissionId) !== pending) return;
3830
- this.inFlightPermissions.delete(permissionId);
3831
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
3832
- this.reevaluateDeferredPermissionTerminal();
3833
- },
3834
- () => this.handleRuntimeFailure(),
3835
- );
3836
- }, remainingMs);
3837
- }
3838
-
3839
- private permissionOwnerIsCurrent(pending: PendingPermission | PendingToolPermission): boolean {
3840
- if (
3841
- this.closed ||
3842
- this.runtimeDead ||
3843
- pending.generation !== this.generation ||
3844
- pending.runtime !== this.runtime
3845
- ) {
3846
- return false;
3847
- }
3848
- if (pending.turnId === undefined) return true;
3849
- return this.activeTurn?.turnId === pending.turnId && !this.activeTurn.terminal;
3850
- }
3851
2706
 
3852
2707
  private hasTerminalConflict(turn: ActiveTurn, includeChildren = true): boolean {
3853
- const ownsTurn = (pending: PendingPermission | PendingToolPermission) =>
3854
- pending.turnId === turn.turnId;
3855
2708
  return (
3856
2709
  turn.awaitingPermissionEvidence ||
3857
- [...this.pendingPermissions.values()].some(ownsTurn) ||
3858
- [...this.inFlightPermissions.values()].some(ownsTurn) ||
3859
- [...this.pendingToolPermissions.values()].some(ownsTurn) ||
3860
- [...this.inFlightToolPermissions.values()].some(ownsTurn) ||
2710
+ this.permissions.hasForTurn(turn.turnId) ||
3861
2711
  turn.activeToolCallIds.size > 0 ||
3862
2712
  turn.steersInFlight > 0 ||
3863
2713
  (includeChildren && Boolean(this.subsessions?.hasActiveChildren()))
@@ -3993,7 +2843,7 @@ export class OmpProviderSession {
3993
2843
  await Promise.allSettled(turn.userLookups);
3994
2844
  this.refreshAmbiguousTerminal(turn);
3995
2845
  if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
3996
- const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
2846
+ const state = await this.usage.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
3997
2847
  if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
3998
2848
  if (!state) {
3999
2849
  turn.operationalTerminalStage = "unresolved";
@@ -4023,10 +2873,7 @@ export class OmpProviderSession {
4023
2873
  ) {
4024
2874
  return;
4025
2875
  }
4026
- turn.usageSampleFloor = this.usageSequence + 1;
4027
- if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
4028
- this.usageSample = null;
4029
- }
2876
+ this.usage.invalidateBeforeNextSample(turn);
4030
2877
  this.publishPendingUsers(turn);
4031
2878
  await this.finishTurn(turn, "completed", undefined, false, false, true);
4032
2879
  }
@@ -4055,7 +2902,7 @@ export class OmpProviderSession {
4055
2902
  this.beginTerminalization(turn, replacement);
4056
2903
  return;
4057
2904
  }
4058
- this.pollUsage(turn);
2905
+ this.usage.poll(turn);
4059
2906
  }
4060
2907
 
4061
2908
  private beginTerminalization(turn: ActiveTurn, candidate: TerminalCandidate): void {
@@ -4069,11 +2916,8 @@ export class OmpProviderSession {
4069
2916
  }
4070
2917
  turn.terminalizing = true;
4071
2918
  this.cancelAmbiguousTerminal(turn);
4072
- turn.usageSampleFloor = this.usageSequence + 1;
4073
- if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
4074
- this.usageSample = null;
4075
- }
4076
- this.stopUsagePoll(turn);
2919
+ this.usage.invalidateBeforeNextSample(turn);
2920
+ this.usage.stopPoll(turn);
4077
2921
  turn.agentEndDeadlineTimer = this.scheduler.set(() => {
4078
2922
  turn.agentEndDeadlineTimer = undefined;
4079
2923
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
@@ -4165,14 +3009,14 @@ export class OmpProviderSession {
4165
3009
  ) {
4166
3010
  this.resetAgentEndProbe(turn);
4167
3011
  turn.deferredAgentEnd = candidate;
4168
- this.pollUsage(turn);
3012
+ this.usage.poll(turn);
4169
3013
  return;
4170
3014
  }
4171
3015
  if (turn.interrupted) {
4172
3016
  await this.completeAgentEnd(turn, candidate.event);
4173
3017
  return;
4174
3018
  }
4175
- const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
3019
+ const state = await this.usage.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4176
3020
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
4177
3021
  if (!turn.interrupted && this.subsessions?.hasActiveChildren()) {
4178
3022
  this.resetAgentEndProbe(turn);
@@ -4185,7 +3029,7 @@ export class OmpProviderSession {
4185
3029
  ) {
4186
3030
  this.resetAgentEndProbe(turn);
4187
3031
  turn.deferredAgentEnd = candidate;
4188
- this.pollUsage(turn);
3032
+ this.usage.poll(turn);
4189
3033
  return;
4190
3034
  }
4191
3035
  if (state) {
@@ -4227,7 +3071,7 @@ export class OmpProviderSession {
4227
3071
  !turn.interrupted &&
4228
3072
  this.activeTurn === turn
4229
3073
  ) {
4230
- const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
3074
+ const state = await this.usage.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4231
3075
  if (
4232
3076
  turn.terminal ||
4233
3077
  this.activeTurn !== turn ||
@@ -4305,9 +3149,9 @@ export class OmpProviderSession {
4305
3149
  turn.steerReady.resolve();
4306
3150
  turn.starting = false;
4307
3151
  turn.terminal = true;
4308
- this.stopUsagePoll(turn);
3152
+ this.usage.stopPoll(turn);
4309
3153
  this.cancelAmbiguousTerminal(turn);
4310
- this.resolveTurnPermissions(turn.turnId);
3154
+ this.permissions.resolveTurnPermissions(turn.turnId);
4311
3155
  this.projector.finishTurn(turn.turnId);
4312
3156
  if (this.activeTurn === turn) this.activeTurn = null;
4313
3157
  }
@@ -4332,7 +3176,7 @@ export class OmpProviderSession {
4332
3176
  turn.starting = false;
4333
3177
  turn.steerReady.resolve();
4334
3178
  this.emit({ type: "session.turn", sessionId: this.id, turnId: turn.turnId, state: "started" });
4335
- if (pollUsage) this.pollUsage(turn);
3179
+ if (pollUsage) this.usage.poll(turn);
4336
3180
  }
4337
3181
  private finishTurn(
4338
3182
  turn: ActiveTurn,
@@ -4362,7 +3206,7 @@ export class OmpProviderSession {
4362
3206
  turn.agentEndPending = false;
4363
3207
  this.cancelLocalOnlyCompletion(turn);
4364
3208
  this.cancelAmbiguousTerminal(turn);
4365
- this.stopUsagePoll(turn);
3209
+ this.usage.stopPoll(turn);
4366
3210
  if (turn.manualCompactionDeadlineTimer !== undefined) {
4367
3211
  this.scheduler.clear(turn.manualCompactionDeadlineTimer);
4368
3212
  turn.manualCompactionDeadlineTimer = undefined;
@@ -4381,7 +3225,7 @@ export class OmpProviderSession {
4381
3225
  const terminalization = (async () => {
4382
3226
  if (!turn.terminalOutcome?.usageSampled && !this.closed && !this.runtimeDead) {
4383
3227
  await Promise.race([
4384
- this.boundedUsageSnapshot(turn, FINAL_USAGE_WAIT_MS),
3228
+ this.usage.boundedSnapshot(turn, FINAL_USAGE_WAIT_MS),
4385
3229
  wake.promise.then(() => undefined),
4386
3230
  ]);
4387
3231
  }
@@ -4408,17 +3252,17 @@ export class OmpProviderSession {
4408
3252
  }
4409
3253
  this.imageMaterializer.clear();
4410
3254
  turn.terminal = true;
4411
- if (this.usageSample?.turn === turn) this.usageSample = null;
4412
- if (!preserveCompactions && this.activeCompaction) {
4413
- if (outcome.state === "canceled") this.finishCompaction("canceled");
3255
+ this.usage.clearSample(turn);
3256
+ if (!preserveCompactions && this.compaction.active) {
3257
+ if (outcome.state === "canceled") this.compaction.finish("canceled");
4414
3258
  else {
4415
- this.finishCompaction("failed", {
3259
+ this.compaction.finish("failed", {
4416
3260
  message: outcome.error?.message ?? "OMP compaction ended without a terminal result",
4417
3261
  });
4418
3262
  }
4419
3263
  }
4420
3264
  this.publishPendingUsers(turn);
4421
- this.resolveTurnPermissions(turn.turnId);
3265
+ this.permissions.resolveTurnPermissions(turn.turnId);
4422
3266
  try {
4423
3267
  this.projector.finishTurn(turn.turnId, preserveCompactions);
4424
3268
  } catch (error) {
@@ -4449,15 +3293,14 @@ export class OmpProviderSession {
4449
3293
  ): void {
4450
3294
  this.imageMaterializer.clear();
4451
3295
  if (this.closed || this.runtimeDead) return;
4452
- this.discardedCompactionEnds = 0;
4453
- this.resolveAllPermissions();
3296
+ this.compaction.resetDiscardedEnds();
3297
+ this.permissions.resolveAllPermissions();
4454
3298
  this.recoveryUsesNativeConfig ||=
4455
3299
  this.configRefreshInFlight !== null || this.configRefreshDirty || this.configMutationInFlight;
4456
3300
  const turn = this.activeTurn;
4457
- if (turn) this.stopUsagePoll(turn);
4458
- this.usageSample = null;
4459
- this.finishCompaction(compactionState, { message });
4460
- this.lastUsage = null;
3301
+ if (turn) this.usage.stopPoll(turn);
3302
+ this.usage.reset();
3303
+ this.compaction.finish(compactionState, { message });
4461
3304
  this.generation += 1;
4462
3305
  this.projector.resetRuntimeGeneration("OMP runtime ended during compaction");
4463
3306
  this.runtimeDead = message;