@omercnet/paseo-omp 0.4.0-next.123.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "0.4.0-next.123.1",
3
+ "version": "0.4.0-next.124.1",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -1,2 +1,2 @@
1
1
  /** Release Please keeps this bundle-safe constant aligned with package.json. */
2
- export const PASEO_OMP_PACKAGE_VERSION = "0.4.0-next.123.1"; // x-release-please-version
2
+ export const PASEO_OMP_PACKAGE_VERSION = "0.4.0-next.124.1"; // x-release-please-version
@@ -0,0 +1,187 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { ProviderEvent } from "@getpaseo/plugin/server/provider";
3
+ import type { OmpRpcEvent } from "./omp-rpc-protocol";
4
+ import type { OmpPublicDataSerializer } from "./security";
5
+ import type { ActiveTurn } from "./session-terminal";
6
+ import type { OmpSessionUsage } from "./session-usage";
7
+ import type { OmpTimelineProjector } from "./timeline-projector";
8
+
9
+ type Emit = (event: ProviderEvent) => void;
10
+
11
+ export interface ActiveCompaction {
12
+ id: string;
13
+ trigger: "auto" | "manual";
14
+ turnId: string;
15
+ generation: number;
16
+ retrying: boolean;
17
+ action?: string;
18
+ preTokens?: number;
19
+ }
20
+
21
+ export class OmpSessionCompaction {
22
+ private operation: ActiveCompaction | null = null;
23
+ private discardedEnds = 0;
24
+
25
+ constructor(
26
+ private readonly sessionId: string,
27
+ private readonly emit: Emit,
28
+ private readonly projector: OmpTimelineProjector,
29
+ private readonly dataFilter: OmpPublicDataSerializer,
30
+ private readonly usage: OmpSessionUsage,
31
+ ) {}
32
+
33
+ get active(): ActiveCompaction | null {
34
+ return this.operation;
35
+ }
36
+
37
+ start(turn: ActiveTurn, trigger: "auto" | "manual", action?: string): void {
38
+ if (this.discardedEnds > 0) {
39
+ this.discardedEnds += 1;
40
+ return;
41
+ }
42
+ const active = this.operation;
43
+ if (active && active.trigger === trigger && active.action === action) {
44
+ active.retrying = false;
45
+ return;
46
+ }
47
+ if (active) {
48
+ this.retire("OMP emitted overlapping compactions");
49
+ this.discardedEnds = 2;
50
+ return;
51
+ }
52
+ const operation: ActiveCompaction = {
53
+ id: randomUUID(),
54
+ trigger,
55
+ turnId: turn.turnId,
56
+ generation: turn.generation,
57
+ action,
58
+ retrying: false,
59
+ preTokens: this.usage.latest?.contextWindowUsedTokens,
60
+ };
61
+ this.operation = operation;
62
+ this.projector.flush(true);
63
+ this.emit({
64
+ type: "timeline.item",
65
+ sessionId: this.sessionId,
66
+ item: {
67
+ id: operation.id,
68
+ type: "compaction",
69
+ status: "loading",
70
+ trigger,
71
+ ...(operation.preTokens !== undefined ? { preTokens: operation.preTokens } : {}),
72
+ },
73
+ });
74
+ }
75
+
76
+ handleAutoEnd(
77
+ turn: ActiveTurn,
78
+ event: Extract<OmpRpcEvent, { type: "auto_compaction_end" }>,
79
+ ): boolean {
80
+ if (this.discardedEnds > 0) {
81
+ this.discardedEnds -= 1;
82
+ return false;
83
+ }
84
+ const operation = this.operation;
85
+ if (
86
+ operation?.trigger !== "auto" ||
87
+ operation.turnId !== turn.turnId ||
88
+ operation.generation !== turn.generation
89
+ ) {
90
+ return false;
91
+ }
92
+ if (event.action !== undefined && operation.action !== event.action) {
93
+ this.retire("OMP emitted overlapping compactions");
94
+ this.discardedEnds = 1;
95
+ return false;
96
+ }
97
+ if (event.willRetry) {
98
+ operation.retrying = true;
99
+ return false;
100
+ }
101
+ if (operation.retrying) {
102
+ operation.retrying = false;
103
+ return false;
104
+ }
105
+ const state = event.aborted
106
+ ? "canceled"
107
+ : event.errorMessage
108
+ ? "failed"
109
+ : event.skipped
110
+ ? "skipped"
111
+ : "completed";
112
+ this.finish(state, {
113
+ tokensBefore: event.result?.tokensBefore ?? event.result?.preTokens,
114
+ message: event.errorMessage,
115
+ });
116
+ return true;
117
+ }
118
+
119
+ finish(
120
+ state: "completed" | "failed" | "canceled" | "skipped",
121
+ options: { tokensBefore?: number | null; message?: string } = {},
122
+ ): void {
123
+ const operation = this.operation;
124
+ if (!operation) return;
125
+ this.operation = null;
126
+ this.projector.flush(true);
127
+ if (state === "completed") {
128
+ this.usage.completeCompaction();
129
+ }
130
+ if (state !== "completed") {
131
+ const defaultMessage =
132
+ state === "failed"
133
+ ? "OMP compaction failed"
134
+ : state === "canceled"
135
+ ? "OMP compaction canceled"
136
+ : "OMP compaction skipped";
137
+ this.emit({
138
+ type: "timeline.item",
139
+ sessionId: this.sessionId,
140
+ item: {
141
+ id: operation.id,
142
+ type: "notification",
143
+ level: state === "failed" ? "error" : "info",
144
+ message: this.dataFilter.text(options.message ?? defaultMessage, 4_096),
145
+ },
146
+ });
147
+ return;
148
+ }
149
+ const tokensBefore = options.tokensBefore ?? operation.preTokens;
150
+ this.emit({
151
+ type: "timeline.item",
152
+ sessionId: this.sessionId,
153
+ item: {
154
+ id: operation.id,
155
+ type: "compaction",
156
+ status: "completed",
157
+ trigger: operation.trigger,
158
+ ...(tokensBefore !== undefined ? { preTokens: tokensBefore } : {}),
159
+ },
160
+ });
161
+ }
162
+
163
+ retire(message: string): void {
164
+ const operation = this.operation;
165
+ if (!operation) return;
166
+ this.operation = null;
167
+ this.emit({
168
+ type: "timeline.item",
169
+ sessionId: this.sessionId,
170
+ item: {
171
+ id: operation.id,
172
+ type: "compaction",
173
+ status: "completed",
174
+ trigger: operation.trigger,
175
+ },
176
+ });
177
+ this.emit({
178
+ type: "timeline.item",
179
+ sessionId: this.sessionId,
180
+ item: { id: `${operation.id}:error`, type: "error", message },
181
+ });
182
+ }
183
+
184
+ resetDiscardedEnds(): void {
185
+ this.discardedEnds = 0;
186
+ }
187
+ }
@@ -0,0 +1,234 @@
1
+ import type { ProviderEvent, ProviderUsage } from "@getpaseo/plugin/server/provider";
2
+ import type { OmpRuntimeSession } from "./omp-rpc";
3
+ import type { OmpSessionState, OmpSessionStats } from "./omp-rpc-protocol";
4
+ import type { ActiveTurn } from "./session-terminal";
5
+ import type { OmpTimelineScheduler } from "./timeline-projector";
6
+
7
+ const USAGE_POLL_MS = 1_000;
8
+
9
+ interface UsageContext {
10
+ closed: boolean;
11
+ runtimeDead: boolean;
12
+ generation: number;
13
+ runtime: OmpRuntimeSession;
14
+ activeTurn: ActiveTurn | null;
15
+ }
16
+
17
+ type Emit = (event: ProviderEvent) => void;
18
+ type ReadContext = () => UsageContext;
19
+ type IsPollable = (turn: ActiveTurn) => boolean;
20
+
21
+ interface UsageSample {
22
+ turn: ActiveTurn;
23
+ generation: number;
24
+ runtime: OmpRuntimeSession;
25
+ epoch: number;
26
+ sequence: number;
27
+ promise: Promise<OmpSessionState | undefined>;
28
+ }
29
+
30
+ function usageFrom(
31
+ state: OmpSessionState | undefined,
32
+ stats: OmpSessionStats | undefined,
33
+ ): ProviderUsage | undefined {
34
+ if (!state?.contextUsage && !stats) return undefined;
35
+ const stateContext = state?.contextUsage;
36
+ const statsContext = stats?.contextUsage;
37
+ const modelCapacity = state?.model?.contextWindow;
38
+ const inputTokens = stats?.tokens?.input;
39
+ const cachedInputTokens = stats?.tokens?.cacheRead;
40
+ const outputTokens = stats?.tokens?.output;
41
+ const totalCostUsd = stats?.cost;
42
+ const contextTokens =
43
+ typeof stateContext?.tokens === "number"
44
+ ? stateContext.tokens
45
+ : typeof statsContext?.tokens === "number"
46
+ ? statsContext.tokens
47
+ : undefined;
48
+ const contextWindow =
49
+ typeof stateContext?.contextWindow === "number" && stateContext.contextWindow > 0
50
+ ? stateContext.contextWindow
51
+ : typeof statsContext?.contextWindow === "number" && statsContext.contextWindow > 0
52
+ ? statsContext.contextWindow
53
+ : typeof modelCapacity === "number" && modelCapacity > 0
54
+ ? modelCapacity
55
+ : undefined;
56
+ const usage: ProviderUsage = {
57
+ ...(typeof inputTokens === "number" ? { inputTokens } : {}),
58
+ ...(typeof cachedInputTokens === "number" ? { cachedInputTokens } : {}),
59
+ ...(typeof outputTokens === "number" ? { outputTokens } : {}),
60
+ ...(typeof totalCostUsd === "number" ? { totalCostUsd } : {}),
61
+ ...(contextTokens !== undefined ? { contextWindowUsedTokens: contextTokens } : {}),
62
+ ...(contextWindow !== undefined ? { contextWindowMaxTokens: contextWindow } : {}),
63
+ };
64
+ return Object.keys(usage).length > 0 ? usage : undefined;
65
+ }
66
+
67
+ export class OmpSessionUsage {
68
+ private epoch = 0;
69
+ private sequence = 0;
70
+ private sample: UsageSample | null = null;
71
+ private lastUsage: ProviderUsage | null = null;
72
+
73
+ constructor(
74
+ private readonly sessionId: string,
75
+ private readonly emit: Emit,
76
+ private readonly scheduler: OmpTimelineScheduler,
77
+ private readonly readContext: ReadContext,
78
+ private readonly isPollable: IsPollable,
79
+ ) {}
80
+
81
+ get latest(): ProviderUsage | null {
82
+ return this.lastUsage;
83
+ }
84
+
85
+ private ownsSample(turn: ActiveTurn, generation: number, runtime: OmpRuntimeSession): boolean {
86
+ const context = this.readContext();
87
+ return (
88
+ !context.closed &&
89
+ !context.runtimeDead &&
90
+ !turn.terminal &&
91
+ generation === context.generation &&
92
+ runtime === context.runtime &&
93
+ context.activeTurn === turn
94
+ );
95
+ }
96
+
97
+ publishSnapshot(
98
+ turn: ActiveTurn,
99
+ minimumEpoch = this.epoch,
100
+ minimumSequence = turn.usageSampleFloor,
101
+ ): Promise<OmpSessionState | undefined> {
102
+ const { generation, runtime } = this.readContext();
103
+ if (!this.ownsSample(turn, generation, runtime)) return Promise.resolve(undefined);
104
+ const current = this.sample;
105
+ if (current) {
106
+ if (
107
+ current.turn === turn &&
108
+ current.generation === generation &&
109
+ current.runtime === runtime &&
110
+ current.epoch >= minimumEpoch &&
111
+ current.sequence >= minimumSequence
112
+ ) {
113
+ return current.promise;
114
+ }
115
+ return current.promise.then(() => {
116
+ if (!this.ownsSample(turn, generation, runtime)) return undefined;
117
+ return this.publishSnapshot(turn, minimumEpoch, minimumSequence);
118
+ });
119
+ }
120
+ const epoch = this.epoch;
121
+ const sequence = ++this.sequence;
122
+ const promise = Promise.allSettled([runtime.getState(), runtime.getSessionStats()]).then(
123
+ ([stateResult, statsResult]) => {
124
+ if (
125
+ !this.ownsSample(turn, generation, runtime) ||
126
+ epoch !== this.epoch ||
127
+ epoch < minimumEpoch ||
128
+ sequence < turn.usageSampleFloor ||
129
+ sequence < minimumSequence
130
+ ) {
131
+ return undefined;
132
+ }
133
+ const state = stateResult.status === "fulfilled" ? stateResult.value : undefined;
134
+ const stats = statsResult.status === "fulfilled" ? statsResult.value : undefined;
135
+ const usage = usageFrom(state, stats);
136
+ if (usage) {
137
+ this.lastUsage = usage;
138
+ this.emit({
139
+ type: "session.usage",
140
+ sessionId: this.sessionId,
141
+ turnId: turn.turnId,
142
+ usage,
143
+ });
144
+ }
145
+ return state;
146
+ },
147
+ );
148
+ this.sample = { turn, generation, runtime, epoch, sequence, promise };
149
+ void promise.finally(() => {
150
+ if (this.sample?.promise === promise) this.sample = null;
151
+ });
152
+ return promise;
153
+ }
154
+
155
+ async boundedSnapshot(turn: ActiveTurn, timeoutMs: number): Promise<OmpSessionState | undefined> {
156
+ const timeout = Promise.withResolvers<undefined>();
157
+ const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
158
+ try {
159
+ return await Promise.race([this.publishSnapshot(turn), timeout.promise]);
160
+ } finally {
161
+ this.scheduler.clear(timer);
162
+ }
163
+ }
164
+
165
+ async boundedTerminalState(
166
+ turn: ActiveTurn,
167
+ timeoutMs: number,
168
+ ): Promise<OmpSessionState | undefined> {
169
+ const { generation, runtime } = this.readContext();
170
+ const timeout = Promise.withResolvers<undefined>();
171
+ const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
172
+ try {
173
+ const state = await Promise.race([runtime.getState(), timeout.promise]);
174
+ return this.ownsSample(turn, generation, runtime) ? state : undefined;
175
+ } catch {
176
+ return undefined;
177
+ } finally {
178
+ this.scheduler.clear(timer);
179
+ }
180
+ }
181
+
182
+ schedulePoll(turn: ActiveTurn, delayMs = USAGE_POLL_MS): void {
183
+ if (!this.isPollable(turn) || turn.usagePollTimer !== undefined) return;
184
+ turn.usagePollTimer = this.scheduler.set(() => {
185
+ turn.usagePollTimer = undefined;
186
+ this.poll(turn);
187
+ }, delayMs);
188
+ }
189
+
190
+ poll(turn: ActiveTurn): void {
191
+ if (!this.isPollable(turn) || turn.usagePoll) return;
192
+ const poll = this.publishSnapshot(turn).then(() => undefined);
193
+ turn.usagePoll = poll;
194
+ void poll.finally(() => {
195
+ if (turn.usagePoll === poll) turn.usagePoll = undefined;
196
+ this.schedulePoll(turn);
197
+ });
198
+ }
199
+
200
+ stopPoll(turn: ActiveTurn): void {
201
+ if (turn.usagePollTimer === undefined) return;
202
+ this.scheduler.clear(turn.usagePollTimer);
203
+ turn.usagePollTimer = undefined;
204
+ }
205
+
206
+ invalidateBeforeNextSample(turn: ActiveTurn): void {
207
+ turn.usageSampleFloor = this.sequence + 1;
208
+ if (this.sample?.turn === turn && this.sample.sequence < turn.usageSampleFloor) {
209
+ this.sample = null;
210
+ }
211
+ }
212
+
213
+ completeCompaction(): void {
214
+ this.epoch += 1;
215
+ const staleSample = this.sample;
216
+ if (staleSample && staleSample.epoch < this.epoch) {
217
+ this.sample = null;
218
+ staleSample.turn.usagePoll = undefined;
219
+ }
220
+ this.lastUsage = null;
221
+ }
222
+ clearLatest(): void {
223
+ this.lastUsage = null;
224
+ }
225
+
226
+ clearSample(turn: ActiveTurn): void {
227
+ if (this.sample?.turn === turn) this.sample = null;
228
+ }
229
+
230
+ reset(): void {
231
+ this.sample = null;
232
+ this.lastUsage = null;
233
+ }
234
+ }
@@ -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,6 +62,7 @@ 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";
69
68
  import { isNativeTurnActivity, isPassiveUiMethod, isRuntimeConfigEvent } from "./session-events";
@@ -79,6 +78,7 @@ import {
79
78
  terminalOutcome,
80
79
  unknownTerminalOutcomeError,
81
80
  } from "./session-terminal";
81
+ import { OmpSessionUsage } from "./session-usage";
82
82
  import { OmpSubsessionProjector } from "./subsessions";
83
83
  import {
84
84
  defaultOmpTimelineScheduler,
@@ -295,16 +295,6 @@ function permissionFingerprint(request: OmpQuestionRequest): string {
295
295
  return createHash("sha256").update(JSON.stringify(request)).digest("base64url");
296
296
  }
297
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
298
  function providerError(error: unknown, fallback: string): { message: string } {
309
299
  return { message: isOmpPublicError(error) ? error.message : fallback };
310
300
  }
@@ -364,19 +354,8 @@ export class OmpProviderSession {
364
354
  private configRevision = 0;
365
355
  private recoveryUsesNativeConfig = false;
366
356
  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;
357
+ private readonly usage: OmpSessionUsage;
358
+ private readonly compaction: OmpSessionCompaction;
380
359
  private revertInFlight = false;
381
360
  private runtimeTurnCompleted = false;
382
361
  private commandCatalog: OmpAvailableCommand[];
@@ -420,6 +399,26 @@ export class OmpProviderSession {
420
399
  this.id = id;
421
400
  this.cwd = config.cwd;
422
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
+ );
423
422
  this.dataFilter = new OmpPublicDataSerializer(outputRedactionValues);
424
423
  this.nativeModelsByPublicId = nativeModelsByPublicId;
425
424
  this.commandCatalog = commandCatalog;
@@ -433,6 +432,13 @@ export class OmpProviderSession {
433
432
  capabilities.includes("timeline.plugin"),
434
433
  hostTools.labels,
435
434
  );
435
+ this.compaction = new OmpSessionCompaction(
436
+ id,
437
+ emit,
438
+ this.projector,
439
+ this.dataFilter,
440
+ this.usage,
441
+ );
436
442
  this.subsessions = capabilities.includes("session.subsession")
437
443
  ? new OmpSubsessionProjector(
438
444
  id,
@@ -774,289 +780,6 @@ export class OmpProviderSession {
774
780
  });
775
781
  }
776
782
 
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
783
  private async replayHistory(preferPersistedTranscript = false): Promise<void> {
1061
784
  if (!this.runtime.canReplayHistory) {
1062
785
  throw new OmpPublicError("OMP session history cannot be replayed safely");
@@ -1449,8 +1172,8 @@ export class OmpProviderSession {
1449
1172
  turn.starting = false;
1450
1173
  turn.acknowledged = true;
1451
1174
  this.startTurn(turn, false);
1452
- this.startCompaction(turn, "manual");
1453
- this.pollUsage(turn);
1175
+ this.compaction.start(turn, "manual");
1176
+ this.usage.poll(turn);
1454
1177
  const bufferedEvents = turn.bufferedEvents.splice(0);
1455
1178
  turn.replayingBufferedEvents = true;
1456
1179
  for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
@@ -1541,7 +1264,7 @@ export class OmpProviderSession {
1541
1264
  else {
1542
1265
  turn.steerReady.resolve();
1543
1266
  turn.terminal = true;
1544
- this.finishCompaction("failed", { message: "OMP prompt failed" });
1267
+ this.compaction.finish("failed", { message: "OMP prompt failed" });
1545
1268
  this.resolveTurnPermissions(turn.turnId);
1546
1269
  this.projector.finishTurn(turn.turnId);
1547
1270
  if (this.activeTurn === turn) this.activeTurn = null;
@@ -1714,7 +1437,7 @@ export class OmpProviderSession {
1714
1437
  const result = await compaction;
1715
1438
  turn.manualCompactionPending = false;
1716
1439
  if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
1717
- this.finishCompaction("completed", { tokensBefore: result.tokensBefore });
1440
+ this.compaction.finish("completed", { tokensBefore: result.tokensBefore });
1718
1441
  await this.finishTurn(turn, turn.interrupted ? "canceled" : "completed");
1719
1442
  } catch (error) {
1720
1443
  if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
@@ -1722,7 +1445,7 @@ export class OmpProviderSession {
1722
1445
  const raw = providerError(error, "OMP compaction failed");
1723
1446
  const failure = { message: this.dataFilter.text(raw.message, 4_096) };
1724
1447
  const state = turn.interrupted ? "canceled" : "failed";
1725
- this.finishCompaction(state, { message: failure.message });
1448
+ this.compaction.finish(state, { message: failure.message });
1726
1449
  await this.finishTurn(turn, state, state === "failed" ? failure : undefined, true, true);
1727
1450
  }
1728
1451
  }
@@ -2299,11 +2022,11 @@ export class OmpProviderSession {
2299
2022
  if (turn.started) await this.finishTurn(turn, "canceled", undefined, true, true);
2300
2023
  else {
2301
2024
  this.settleUnstartedTurn(turn);
2302
- this.finishCompaction("canceled");
2025
+ this.compaction.finish("canceled");
2303
2026
  }
2304
2027
  }
2305
2028
  this.imageMaterializer.clear();
2306
- this.finishCompaction("canceled");
2029
+ this.compaction.finish("canceled");
2307
2030
  this.resolveAllPermissions(true);
2308
2031
  this.closed = true;
2309
2032
  this.configRefreshAttempts = 0;
@@ -2463,7 +2186,7 @@ export class OmpProviderSession {
2463
2186
  }
2464
2187
  if (this.subsessions) await recovered.setSubagentSubscription("events");
2465
2188
  this.generation += 1;
2466
- this.lastUsage = null;
2189
+ this.usage.clearLatest();
2467
2190
  this.runtimeDead = null;
2468
2191
  this.runtimeDisposal = null;
2469
2192
  unsubscribeBootstrap();
@@ -2810,43 +2533,13 @@ export class OmpProviderSession {
2810
2533
  if (event.type === "auto_compaction_start") {
2811
2534
  turn.nativeActivity = true;
2812
2535
  this.cancelLocalOnlyCompletion(turn);
2813
- this.startCompaction(turn, "auto", event.action);
2536
+ this.compaction.start(turn, "auto", event.action);
2814
2537
  return;
2815
2538
  }
2816
2539
  if (event.type === "auto_compaction_end") {
2817
- if (this.discardedCompactionEnds > 0) {
2818
- this.discardedCompactionEnds -= 1;
2819
- return;
2820
- }
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;
2540
+ if (this.compaction.handleAutoEnd(turn, event)) {
2541
+ this.usage.schedulePoll(turn, USAGE_REFRESH_MS);
2837
2542
  }
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
2543
  return;
2851
2544
  }
2852
2545
  if (event.type === "agent_end" && turn.manualCompactionPending) return;
@@ -3993,7 +3686,7 @@ export class OmpProviderSession {
3993
3686
  await Promise.allSettled(turn.userLookups);
3994
3687
  this.refreshAmbiguousTerminal(turn);
3995
3688
  if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
3996
- const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
3689
+ const state = await this.usage.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
3997
3690
  if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
3998
3691
  if (!state) {
3999
3692
  turn.operationalTerminalStage = "unresolved";
@@ -4023,10 +3716,7 @@ export class OmpProviderSession {
4023
3716
  ) {
4024
3717
  return;
4025
3718
  }
4026
- turn.usageSampleFloor = this.usageSequence + 1;
4027
- if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
4028
- this.usageSample = null;
4029
- }
3719
+ this.usage.invalidateBeforeNextSample(turn);
4030
3720
  this.publishPendingUsers(turn);
4031
3721
  await this.finishTurn(turn, "completed", undefined, false, false, true);
4032
3722
  }
@@ -4055,7 +3745,7 @@ export class OmpProviderSession {
4055
3745
  this.beginTerminalization(turn, replacement);
4056
3746
  return;
4057
3747
  }
4058
- this.pollUsage(turn);
3748
+ this.usage.poll(turn);
4059
3749
  }
4060
3750
 
4061
3751
  private beginTerminalization(turn: ActiveTurn, candidate: TerminalCandidate): void {
@@ -4069,11 +3759,8 @@ export class OmpProviderSession {
4069
3759
  }
4070
3760
  turn.terminalizing = true;
4071
3761
  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);
3762
+ this.usage.invalidateBeforeNextSample(turn);
3763
+ this.usage.stopPoll(turn);
4077
3764
  turn.agentEndDeadlineTimer = this.scheduler.set(() => {
4078
3765
  turn.agentEndDeadlineTimer = undefined;
4079
3766
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
@@ -4165,14 +3852,14 @@ export class OmpProviderSession {
4165
3852
  ) {
4166
3853
  this.resetAgentEndProbe(turn);
4167
3854
  turn.deferredAgentEnd = candidate;
4168
- this.pollUsage(turn);
3855
+ this.usage.poll(turn);
4169
3856
  return;
4170
3857
  }
4171
3858
  if (turn.interrupted) {
4172
3859
  await this.completeAgentEnd(turn, candidate.event);
4173
3860
  return;
4174
3861
  }
4175
- const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
3862
+ const state = await this.usage.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4176
3863
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
4177
3864
  if (!turn.interrupted && this.subsessions?.hasActiveChildren()) {
4178
3865
  this.resetAgentEndProbe(turn);
@@ -4185,7 +3872,7 @@ export class OmpProviderSession {
4185
3872
  ) {
4186
3873
  this.resetAgentEndProbe(turn);
4187
3874
  turn.deferredAgentEnd = candidate;
4188
- this.pollUsage(turn);
3875
+ this.usage.poll(turn);
4189
3876
  return;
4190
3877
  }
4191
3878
  if (state) {
@@ -4227,7 +3914,7 @@ export class OmpProviderSession {
4227
3914
  !turn.interrupted &&
4228
3915
  this.activeTurn === turn
4229
3916
  ) {
4230
- const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
3917
+ const state = await this.usage.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4231
3918
  if (
4232
3919
  turn.terminal ||
4233
3920
  this.activeTurn !== turn ||
@@ -4305,7 +3992,7 @@ export class OmpProviderSession {
4305
3992
  turn.steerReady.resolve();
4306
3993
  turn.starting = false;
4307
3994
  turn.terminal = true;
4308
- this.stopUsagePoll(turn);
3995
+ this.usage.stopPoll(turn);
4309
3996
  this.cancelAmbiguousTerminal(turn);
4310
3997
  this.resolveTurnPermissions(turn.turnId);
4311
3998
  this.projector.finishTurn(turn.turnId);
@@ -4332,7 +4019,7 @@ export class OmpProviderSession {
4332
4019
  turn.starting = false;
4333
4020
  turn.steerReady.resolve();
4334
4021
  this.emit({ type: "session.turn", sessionId: this.id, turnId: turn.turnId, state: "started" });
4335
- if (pollUsage) this.pollUsage(turn);
4022
+ if (pollUsage) this.usage.poll(turn);
4336
4023
  }
4337
4024
  private finishTurn(
4338
4025
  turn: ActiveTurn,
@@ -4362,7 +4049,7 @@ export class OmpProviderSession {
4362
4049
  turn.agentEndPending = false;
4363
4050
  this.cancelLocalOnlyCompletion(turn);
4364
4051
  this.cancelAmbiguousTerminal(turn);
4365
- this.stopUsagePoll(turn);
4052
+ this.usage.stopPoll(turn);
4366
4053
  if (turn.manualCompactionDeadlineTimer !== undefined) {
4367
4054
  this.scheduler.clear(turn.manualCompactionDeadlineTimer);
4368
4055
  turn.manualCompactionDeadlineTimer = undefined;
@@ -4381,7 +4068,7 @@ export class OmpProviderSession {
4381
4068
  const terminalization = (async () => {
4382
4069
  if (!turn.terminalOutcome?.usageSampled && !this.closed && !this.runtimeDead) {
4383
4070
  await Promise.race([
4384
- this.boundedUsageSnapshot(turn, FINAL_USAGE_WAIT_MS),
4071
+ this.usage.boundedSnapshot(turn, FINAL_USAGE_WAIT_MS),
4385
4072
  wake.promise.then(() => undefined),
4386
4073
  ]);
4387
4074
  }
@@ -4408,11 +4095,11 @@ export class OmpProviderSession {
4408
4095
  }
4409
4096
  this.imageMaterializer.clear();
4410
4097
  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");
4098
+ this.usage.clearSample(turn);
4099
+ if (!preserveCompactions && this.compaction.active) {
4100
+ if (outcome.state === "canceled") this.compaction.finish("canceled");
4414
4101
  else {
4415
- this.finishCompaction("failed", {
4102
+ this.compaction.finish("failed", {
4416
4103
  message: outcome.error?.message ?? "OMP compaction ended without a terminal result",
4417
4104
  });
4418
4105
  }
@@ -4449,15 +4136,14 @@ export class OmpProviderSession {
4449
4136
  ): void {
4450
4137
  this.imageMaterializer.clear();
4451
4138
  if (this.closed || this.runtimeDead) return;
4452
- this.discardedCompactionEnds = 0;
4139
+ this.compaction.resetDiscardedEnds();
4453
4140
  this.resolveAllPermissions();
4454
4141
  this.recoveryUsesNativeConfig ||=
4455
4142
  this.configRefreshInFlight !== null || this.configRefreshDirty || this.configMutationInFlight;
4456
4143
  const turn = this.activeTurn;
4457
- if (turn) this.stopUsagePoll(turn);
4458
- this.usageSample = null;
4459
- this.finishCompaction(compactionState, { message });
4460
- this.lastUsage = null;
4144
+ if (turn) this.usage.stopPoll(turn);
4145
+ this.usage.reset();
4146
+ this.compaction.finish(compactionState, { message });
4461
4147
  this.generation += 1;
4462
4148
  this.projector.resetRuntimeGeneration("OMP runtime ended during compaction");
4463
4149
  this.runtimeDead = message;