@omercnet/paseo-omp 0.4.0 → 1.0.0-next.160.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.
Files changed (40) hide show
  1. package/README.md +8 -8
  2. package/client/external-url.ts +2 -11
  3. package/client/omp-config-styles.ts +284 -0
  4. package/client/omp-config-surface.tsx +16 -286
  5. package/client/omp-routing-editor.tsx +1 -3
  6. package/client/provider-launch-settings.tsx +118 -0
  7. package/index.client.tsx +21 -2
  8. package/index.server.ts +10 -0
  9. package/package.json +14 -9
  10. package/paseo-plugin.json +2 -1
  11. package/scripts/prepare-dependencies.mjs +44 -11
  12. package/server/omp-models.ts +2 -6
  13. package/server/package-version.ts +1 -1
  14. package/server/protocol-violation-diagnostics.ts +1 -1
  15. package/server/provider/catalog.ts +2 -1
  16. package/server/provider/config-normalization.ts +19 -3
  17. package/server/provider/connection.ts +30 -0
  18. package/server/provider/host-tools.ts +2 -2
  19. package/server/provider/mcp-transport.ts +9 -2
  20. package/server/provider/omp-rpc-environment.ts +368 -0
  21. package/server/provider/omp-rpc-process.ts +119 -0
  22. package/server/provider/omp-rpc-protocol.ts +1949 -0
  23. package/server/provider/omp-rpc-transport.ts +1373 -0
  24. package/server/provider/omp-rpc-values.ts +31 -0
  25. package/server/provider/omp-rpc.ts +267 -3311
  26. package/server/provider/profile-providers.ts +2 -1
  27. package/server/provider/prompt-payload.ts +253 -0
  28. package/server/provider/registration.ts +10 -2
  29. package/server/provider/session-compaction.ts +187 -0
  30. package/server/provider/session-descriptors.ts +52 -18
  31. package/server/provider/session-events.ts +42 -0
  32. package/server/provider/session-permissions.ts +987 -0
  33. package/server/provider/session-terminal.ts +307 -0
  34. package/server/provider/session-usage.ts +234 -0
  35. package/server/provider/session.ts +145 -1853
  36. package/server/provider/settings.ts +8 -5
  37. package/server/provider/subsessions.ts +30 -9
  38. package/server/provider/timeline-projector.ts +140 -29
  39. package/shared/external-url.ts +0 -9
  40. package/shared/provider-launch-settings.ts +18 -0
@@ -1,16 +1,11 @@
1
- import { createHash, randomUUID } from "node:crypto";
1
+ import { randomUUID } from "node:crypto";
2
2
  import type {
3
3
  ProviderConfigState,
4
- ProviderContent,
5
4
  ProviderError,
6
5
  ProviderEvent,
7
6
  ProviderInput,
8
- ProviderPermissionResponse,
9
7
  ProviderSessionConfig,
10
- ProviderToolCallDetail,
11
- ProviderUsage,
12
8
  } from "@getpaseo/plugin/server/provider";
13
- import { getForgeDefinitionOrNeutral } from "@getpaseo/protocol/forge-manifest";
14
9
  import type {
15
10
  OmpOperationalFailure,
16
11
  OmpOperationalFailureReporter,
@@ -30,25 +25,27 @@ import {
30
25
  withCommittedOmpSelection,
31
26
  } from "./config-normalization";
32
27
  import { OmpHostToolsBridge, type OmpMcpConnector, validateOmpHostToolConfig } from "./host-tools";
33
- import { isOmpImageMimeType, isValidImagePayload, OmpImageMaterializer } from "./image";
28
+ import { isOmpImageMimeType, OmpImageMaterializer } from "./image";
29
+ import type { OmpRuntime, OmpRuntimeSession } from "./omp-rpc";
30
+ import { buildOmpSpawnRequest, type OmpStartOptions } from "./omp-rpc-environment";
34
31
  import type {
35
32
  OmpAvailableCommand,
36
33
  OmpBranchResult,
37
34
  OmpCompactionResult,
38
- OmpExtensionUiResponse,
39
- OmpImage,
40
35
  OmpMessage,
41
36
  OmpModel,
42
37
  OmpRpcEvent,
43
- OmpRuntime,
44
- OmpRuntimeSession,
45
38
  OmpSessionState,
46
- OmpSessionStats,
47
- OmpStartOptions,
48
- OmpToolApprovalCancel,
49
- OmpToolApprovalRequest,
50
- } from "./omp-rpc";
51
- import { buildOmpSpawnRequest, OmpRpcRequestRejectedError } from "./omp-rpc";
39
+ } from "./omp-rpc-protocol";
40
+ import { OmpRpcRequestRejectedError } from "./omp-rpc-transport";
41
+ import {
42
+ inlinePromptFrameBytes,
43
+ isSafeCommandName,
44
+ MAX_PROMPT_TEXT_LENGTH,
45
+ type OmpPromptPayload,
46
+ promptPayload,
47
+ slashCommandName,
48
+ } from "./prompt-payload";
52
49
  import {
53
50
  BoundedStringSet,
54
51
  boundedJsonBytes,
@@ -60,8 +57,24 @@ import {
60
57
  OmpPublicError,
61
58
  utf8Bytes,
62
59
  } from "./security";
60
+ import { OmpSessionCompaction } from "./session-compaction";
63
61
  import type { OmpSessionDescriptor } from "./session-descriptors";
64
62
  import { validateNativeSessionId } from "./session-descriptors";
63
+ import { isNativeTurnActivity, isPassiveUiMethod, isRuntimeConfigEvent } from "./session-events";
64
+ import { OmpSessionPermissions } from "./session-permissions";
65
+ import {
66
+ type ActiveTurn,
67
+ assistantTerminalOutcome,
68
+ classifyTerminalCandidate,
69
+ createActiveTurn,
70
+ historyTerminalOutcome,
71
+ nativeEntryId,
72
+ type PendingUser,
73
+ type TerminalCandidate,
74
+ terminalOutcome,
75
+ unknownTerminalOutcomeError,
76
+ } from "./session-terminal";
77
+ import { OmpSessionUsage } from "./session-usage";
65
78
  import { OmpSubsessionProjector } from "./subsessions";
66
79
  import {
67
80
  defaultOmpTimelineScheduler,
@@ -79,10 +92,6 @@ type NativeSessionTransition = (previousSessionId: string, nextSessionId: string
79
92
  type RewindCleanupQuarantine = (cleanup: Promise<void>) => void;
80
93
  type RewindSessionRetirement = () => void;
81
94
  type SessionPermissionInput = Extract<ProviderInput, { type: "session.permission" }>;
82
- type OmpQuestionRequest = Extract<
83
- Extract<OmpRpcEvent, { type: "extension_ui_request" }>,
84
- { method: "select" | "confirm" | "input" | "editor" }
85
- >;
86
95
  type Emit = (event: ProviderEvent) => void;
87
96
  const LOCAL_ONLY_SETTLE_MS = 5_000;
88
97
  const AGENT_END_STATE_TIMEOUT_MS = 2_000;
@@ -94,15 +103,12 @@ const USAGE_REFRESH_MS = 100;
94
103
  const FINAL_USAGE_WAIT_MS = 250;
95
104
  const COMPACTION_MAX_WAIT_MS = 5 * 60_000;
96
105
  const AGENT_END_SETTLE_MS = 5_000;
97
- const MAX_PROMPT_PARTS = 64;
98
- const MAX_PROMPT_TEXT_LENGTH = 1024 * 1024;
99
- const RPC_REQUEST_ID_BYTES = 36;
100
106
  const MAX_AGENT_END_CORRELATION_MESSAGES = 512;
101
107
  const MAX_TRACKED_ENTRY_IDS = 1_024;
102
108
  const MAX_UNCLAIMED_BRANCH_ENTRIES = 1_024;
103
109
  const MAX_PENDING_USERS = 256;
104
- const MAX_PENDING_PERMISSIONS = 32;
105
- const MAX_PENDING_PERMISSION_BYTES = 2 * 1024 * 1024;
110
+ const _MAX_PENDING_PERMISSIONS = 32;
111
+ const _MAX_PENDING_PERMISSION_BYTES = 2 * 1024 * 1024;
106
112
  const MAX_USER_ECHOES = 512;
107
113
  const MAX_BUFFERED_TURN_EVENTS = 512;
108
114
  const MAX_BUFFERED_VALUE_ITEMS = 1_024;
@@ -114,8 +120,8 @@ const MAX_UNCLAIMED_BRANCH_BYTES = 4 * 1024 * 1024;
114
120
  class OmpCatalogEscape extends OmpPublicError {}
115
121
  const MAX_REPLAY_MESSAGES = 100_000;
116
122
  const REPLAY_TIMEOUT_MS = 20_000;
117
- const OMP_ASK_USER_FREEFORM_SENTINEL = "✏️ Type custom response...";
118
- 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;
119
125
  const OMP_BUILTIN_COMMANDS: readonly OmpAvailableCommand[] = [
120
126
  {
121
127
  name: "compact",
@@ -234,86 +240,6 @@ async function waitForReplay<T>(operation: Promise<T>, signal: AbortSignal): Pro
234
240
  }
235
241
  }
236
242
 
237
- type PendingUser = {
238
- clientMessageId: string;
239
- text: string;
240
- accepted: boolean;
241
- fallbackOnFinish: boolean;
242
- bufferedEchoes: OmpMessage[];
243
- };
244
- type TerminalCorrelationEvidence = "fresh-native-user" | "current-assistant";
245
-
246
- type TerminalCorrelation = {
247
- policy: "initial-turn" | "ordered-legacy" | "native-command";
248
- evidence: Map<TerminalCorrelationEvidence, number>;
249
- };
250
-
251
- type TerminalCandidate = {
252
- event: Extract<OmpRpcEvent, { type: "agent_end" }>;
253
- arrivalSequence: number;
254
- confidence:
255
- | "keyed"
256
- | "initial-turn"
257
- | "ordered-legacy"
258
- | "native-command"
259
- | "interrupted"
260
- | "ambiguous";
261
- };
262
-
263
- type ActiveTurn = {
264
- turnId: string;
265
- clientMessageId: string;
266
- generation: number;
267
- promptResultEmitted: boolean;
268
- started: boolean;
269
- terminal: boolean;
270
- interrupted: boolean;
271
- starting: boolean;
272
- nativeActivity: boolean;
273
- userEchoObserved: boolean;
274
- localOnlyDisabled: boolean;
275
- localOnlyEligible: boolean;
276
- awaitingPermissionEvidence: boolean;
277
- activitySequence: number;
278
- acknowledged: boolean;
279
- terminalCorrelation: TerminalCorrelation;
280
- replayingBufferedEvents: boolean;
281
- agentInvoked?: boolean;
282
- nativeRequestId?: string;
283
- promptAcceptedEventIndex?: number;
284
- localOnlyTimer?: unknown;
285
- usagePollTimer?: unknown;
286
- usagePoll?: Promise<void>;
287
- usageSampleFloor: number;
288
- manualCompaction: boolean;
289
- manualCompactionPending: boolean;
290
- manualCompactionDeadlineTimer?: unknown;
291
- agentEndPending: boolean;
292
- agentEndRetryTimer?: unknown;
293
- agentEndDeadlineTimer?: unknown;
294
- agentEndCheck?: Promise<void>;
295
- ambiguousTerminalTimer?: unknown;
296
- terminalizing: boolean;
297
- terminalization?: Promise<void>;
298
- terminalOutcome?: TurnOutcome;
299
- operationalTerminalStage?: "failed" | "unresolved";
300
- terminalWake?: VoidDeferred;
301
- steerReady: VoidDeferred;
302
- steersInFlight: number;
303
- deferredAgentEnd?: TerminalCandidate;
304
- activeToolCallIds: Set<string>;
305
- bufferedEvents: OmpRpcEvent[];
306
- pendingUsers: PendingUser[];
307
- userEchoes: OmpMessage[];
308
- userCorrelationActive: boolean;
309
- userLookups: Set<Promise<void>>;
310
- completedMessageCount: number;
311
- streamedMessageEntryIds: string[];
312
- streamedMessageIdentityComplete: boolean;
313
- lastCompletedAssistantOutcome?: AgentEndOutcome;
314
- lastCompletedAssistantEntryId?: string;
315
- };
316
-
317
243
  type PendingAbort = {
318
244
  turn: ActiveTurn;
319
245
  generation: number;
@@ -321,68 +247,6 @@ type PendingAbort = {
321
247
  forceTerminal: boolean;
322
248
  promise: Promise<void>;
323
249
  };
324
- type PendingPermission = {
325
- nativeId: string;
326
- header: string;
327
- fingerprint: string;
328
- optionValues: ReadonlyMap<string, string>;
329
- actionBehaviors: ReadonlyMap<string, "allow" | "deny">;
330
- retainedBytes: number;
331
- displayValues: ReadonlyMap<string, string>;
332
- generation: number;
333
- runtime: OmpRuntimeSession;
334
- expiresAt?: number;
335
- timer?: unknown;
336
- turnId?: string;
337
- request: OmpQuestionRequest;
338
- freeformSentinel?: string;
339
- };
340
- type PendingToolPermission = {
341
- nativeId: string;
342
- toolCallId: string;
343
- fingerprint: string;
344
- retainedBytes: number;
345
- generation: number;
346
- runtime: OmpRuntimeSession;
347
- expiresAt?: number;
348
- timer?: unknown;
349
- turnId?: string;
350
- };
351
-
352
- type PendingFreeformSelection = {
353
- value: string;
354
- nativeSelectId: string;
355
- generation: number;
356
- runtime: OmpRuntimeSession;
357
- turnId?: string;
358
- };
359
-
360
- function permissionFingerprint(request: OmpQuestionRequest): string {
361
- return createHash("sha256").update(JSON.stringify(request)).digest("base64url");
362
- }
363
-
364
- type VoidDeferred = {
365
- promise: Promise<void>;
366
- resolve(value?: void | PromiseLike<void>): void;
367
- reject(reason?: unknown): void;
368
- };
369
-
370
- type TurnOutcome = {
371
- state: "completed" | "failed" | "canceled";
372
- error?: ProviderError;
373
- usageSampled: boolean;
374
- };
375
-
376
- type ActiveCompaction = {
377
- id: string;
378
- trigger: "auto" | "manual";
379
- turnId: string;
380
- generation: number;
381
- retrying: boolean;
382
- action?: string;
383
- preTokens?: number;
384
- };
385
-
386
250
  function providerError(error: unknown, fallback: string): { message: string } {
387
251
  return { message: isOmpPublicError(error) ? error.message : fallback };
388
252
  }
@@ -407,492 +271,6 @@ async function settleSessionCleanup(promises: readonly Promise<void>[]): Promise
407
271
  throw new AggregateError(failures, "OMP session initialization cleanup failed");
408
272
  }
409
273
  }
410
- function isSafeCommandName(name: string): boolean {
411
- return /^[A-Za-z0-9][A-Za-z0-9._-]*(?::[A-Za-z0-9][A-Za-z0-9._-]*)*$/u.test(name);
412
- }
413
-
414
- type OmpPromptPayload = { text: string; images: OmpImage[]; commandName?: string };
415
-
416
- const REVIEW_LINE_MARKERS = { add: "+", remove: "-", context: " " } as const;
417
-
418
- function renderPromptAttachmentAsText(part: Exclude<ProviderContent, { type: "image" }>): string {
419
- switch (part.type) {
420
- case "forge_change_request": {
421
- return renderChangeRequestAttachment({
422
- forge: part.forge ?? "github",
423
- number: part.number,
424
- title: part.title,
425
- url: part.url,
426
- body: part.body,
427
- projectPath: part.projectPath,
428
- baseRefName: part.baseRefName,
429
- headRefName: part.headRefName,
430
- });
431
- }
432
- case "github_pr": {
433
- return renderChangeRequestAttachment({
434
- forge: "github",
435
- number: part.number,
436
- title: part.title,
437
- url: part.url,
438
- body: part.body,
439
- baseRefName: part.baseRefName,
440
- headRefName: part.headRefName,
441
- });
442
- }
443
- case "forge_issue": {
444
- return renderIssueAttachment({
445
- forge: part.forge ?? "github",
446
- number: part.number,
447
- title: part.title,
448
- url: part.url,
449
- body: part.body,
450
- projectPath: part.projectPath,
451
- });
452
- }
453
- case "github_issue": {
454
- return renderIssueAttachment({
455
- forge: "github",
456
- number: part.number,
457
- title: part.title,
458
- url: part.url,
459
- body: part.body,
460
- });
461
- }
462
- case "text": {
463
- return part.text;
464
- }
465
- case "review": {
466
- const lines = [`Paseo review attachment (${part.mode})`, `CWD: ${part.cwd}`];
467
- if (part.baseRef) {
468
- lines.push(`Base: ${part.baseRef}`);
469
- }
470
- part.comments.forEach((comment, index) => {
471
- lines.push(
472
- "",
473
- `Comment ${index + 1}: ${comment.filePath}:${comment.side}:${comment.lineNumber}`,
474
- comment.body,
475
- comment.context.hunkHeader,
476
- );
477
- const target = comment.context.targetLine;
478
- for (const line of comment.context.lines) {
479
- const isTarget =
480
- line.oldLineNumber === target.oldLineNumber &&
481
- line.newLineNumber === target.newLineNumber &&
482
- line.type === target.type &&
483
- line.content === target.content;
484
- const prefix = isTarget ? "> " : " ";
485
- const oldLn = padLineNumber(line.oldLineNumber);
486
- const newLn = padLineNumber(line.newLineNumber);
487
- lines.push(`${prefix}${oldLn} ${newLn} ${REVIEW_LINE_MARKERS[line.type]}${line.content}`);
488
- }
489
- });
490
- return lines.join("\n");
491
- }
492
- case "uploaded_file": {
493
- return [
494
- `Uploaded file: ${part.fileName}`,
495
- `Path: ${part.path}`,
496
- `MIME: ${part.mimeType}`,
497
- `Size: ${part.size} bytes`,
498
- ].join("\n");
499
- }
500
- default:
501
- throw new Error("unreachable");
502
- }
503
- }
504
-
505
- function renderChangeRequestAttachment(input: {
506
- forge: string;
507
- number: number;
508
- title: string;
509
- url: string;
510
- body?: string | null;
511
- projectPath?: string;
512
- baseRefName?: string | null;
513
- headRefName?: string | null;
514
- }): string {
515
- const lines = [
516
- `${formatForgeLabel(input.forge)} ${formatChangeRequestAbbrev(input.forge)} ${formatChangeRequestNumber(input.forge, input.number)}: ${input.title}`,
517
- input.url,
518
- ];
519
- if (input.projectPath) {
520
- lines.push(`Project: ${input.projectPath}`);
521
- }
522
- if (input.baseRefName) {
523
- lines.push(`Base: ${input.baseRefName}`);
524
- }
525
- if (input.headRefName) {
526
- lines.push(`Head: ${input.headRefName}`);
527
- }
528
- if (input.body) {
529
- lines.push("", input.body);
530
- }
531
- return lines.join("\n");
532
- }
533
-
534
- function renderIssueAttachment(input: {
535
- forge: string;
536
- number: number;
537
- title: string;
538
- url: string;
539
- body?: string | null;
540
- projectPath?: string;
541
- }): string {
542
- const lines = [
543
- `${formatForgeLabel(input.forge)} Issue ${formatIssueNumber(input.forge, input.number)}: ${input.title}`,
544
- input.url,
545
- ];
546
- if (input.projectPath) {
547
- lines.push(`Project: ${input.projectPath}`);
548
- }
549
- if (input.body) {
550
- lines.push("", input.body);
551
- }
552
- return lines.join("\n");
553
- }
554
-
555
- function formatForgeLabel(forge: string): string {
556
- return getForgeDefinitionOrNeutral(forge).displayName;
557
- }
558
-
559
- function formatChangeRequestAbbrev(forge: string): string {
560
- return getForgeDefinitionOrNeutral(forge).changeRequestAbbrev;
561
- }
562
-
563
- function formatChangeRequestNumber(forge: string, number: number): string {
564
- return `${getForgeDefinitionOrNeutral(forge).changeRequestNumberPrefix}${number}`;
565
- }
566
-
567
- function formatIssueNumber(forge: string, number: number): string {
568
- return `${getForgeDefinitionOrNeutral(forge).issueNumberPrefix}${number}`;
569
- }
570
-
571
- function padLineNumber(lineNumber: number | null): string {
572
- return (lineNumber?.toString() ?? "-").padStart(2);
573
- }
574
-
575
- function promptPayload(input: SessionPromptInput): OmpPromptPayload {
576
- if (input.prompt.outputSchema !== undefined || input.prompt.clearPendingPermissions) {
577
- throw new OmpPublicError("OMP does not support structured output or permission controls");
578
- }
579
- if (input.prompt.input.type === "command") {
580
- const name = input.prompt.input.name.trim();
581
- if (!isSafeCommandName(name)) throw new OmpPublicError("Invalid OMP command name");
582
- const argumentsText = input.prompt.input.arguments.trim();
583
- const text = `/${name}${argumentsText ? ` ${argumentsText}` : ""}`;
584
- if (utf8Bytes(text) > MAX_PROMPT_TEXT_LENGTH)
585
- throw new OmpPublicError("OMP command is too large");
586
- return { text, images: [], commandName: name };
587
- }
588
- if (input.prompt.input.content.length > MAX_PROMPT_PARTS) {
589
- throw new OmpPublicError("OMP prompt has too many content parts");
590
- }
591
- const parts: string[] = [];
592
- const images: OmpImage[] = [];
593
- let length = 0;
594
- const appendText = (text: string) => {
595
- length += utf8Bytes(text) + (parts.length > 0 ? 2 : 0);
596
- if (length > MAX_PROMPT_TEXT_LENGTH) throw new OmpPublicError("OMP prompt is too large");
597
- parts.push(text);
598
- };
599
- for (const part of input.prompt.input.content) {
600
- if (part.type === "text") {
601
- appendText(part.text);
602
- continue;
603
- }
604
- if (part.type === "image") {
605
- if (!isValidImagePayload(part.data, part.mimeType, 8 * 1024 * 1024)) {
606
- throw new OmpPublicError("OMP prompt image is invalid");
607
- }
608
- images.push({ type: "image", data: part.data, mimeType: part.mimeType });
609
- continue;
610
- }
611
- appendText(renderPromptAttachmentAsText(part));
612
- }
613
- const text = parts.join("\n\n").trim();
614
- if (!text && images.length === 0) throw new OmpPublicError("OMP prompt cannot be empty");
615
- return { text, images };
616
- }
617
-
618
- function inlinePromptFrameBytes(payload: OmpPromptPayload, delivery: "prompt" | "steer"): number {
619
- const images = payload.images.map(({ type, mimeType }) => ({ type, data: "", mimeType }));
620
- const frame = {
621
- type: delivery,
622
- message: payload.text,
623
- ...(images.length > 0 ? { images } : {}),
624
- ...(delivery === "prompt" ? { id: "" } : {}),
625
- };
626
- const requestIdBytes = delivery === "prompt" ? RPC_REQUEST_ID_BYTES : 0;
627
- return (
628
- utf8Bytes(JSON.stringify(frame)) +
629
- requestIdBytes +
630
- payload.images.reduce((total, image) => total + image.data.length, 0) +
631
- 1
632
- );
633
- }
634
-
635
- function slashCommandName(text: string): string | undefined {
636
- if (!text.startsWith("/")) return undefined;
637
- const body = text.slice(1);
638
- if (!body) return undefined;
639
- const firstWhitespace = body.search(/\s/u);
640
- const name = firstWhitespace === -1 ? body : body.slice(0, firstWhitespace);
641
- return name || undefined;
642
- }
643
-
644
- function nativeEntryId(message: OmpMessage): string | undefined {
645
- return message.entryId;
646
- }
647
-
648
- type AgentEndOutcome = "completed" | "failed" | "canceled";
649
- type AssistantTerminalStatus = AgentEndOutcome | "unavailable";
650
-
651
- function assistantTerminalOutcome(
652
- message: Extract<OmpMessage, { role: "assistant" }>,
653
- ): AgentEndOutcome {
654
- const stopReason = message.stopReason?.toLowerCase();
655
- if (stopReason === "aborted" || stopReason === "canceled" || stopReason === "cancelled") {
656
- return "canceled";
657
- }
658
- return stopReason === "error" || message.errorMessage ? "failed" : "completed";
659
- }
660
-
661
- function lastAssistantStatus(
662
- messages: readonly OmpMessage[],
663
- startIndex = 0,
664
- ): AssistantTerminalStatus {
665
- for (let index = messages.length - 1; index >= startIndex; index -= 1) {
666
- const message = messages[index];
667
- if (message?.role !== "assistant") continue;
668
- return assistantTerminalOutcome(message);
669
- }
670
- return "unavailable";
671
- }
672
-
673
- function terminalOutcome(
674
- event: Extract<OmpRpcEvent, { type: "agent_end" }>,
675
- turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
676
- ): AgentEndOutcome | undefined {
677
- const messages = event.messages;
678
- if (
679
- messages !== undefined &&
680
- (event.messageCount === undefined || messages.length >= event.messageCount)
681
- ) {
682
- const status = lastAssistantStatus(messages);
683
- return status === "unavailable" ? "completed" : status;
684
- }
685
- if (event.messageCount === 0) return "completed";
686
- if (
687
- turn.lastCompletedAssistantOutcome !== undefined &&
688
- (event.messageCount === undefined || turn.completedMessageCount >= event.messageCount)
689
- ) {
690
- return turn.lastCompletedAssistantOutcome;
691
- }
692
- return undefined;
693
- }
694
-
695
- function historyTerminalOutcome(
696
- messages: readonly OmpMessage[],
697
- declaredCount: number,
698
- turn: Pick<
699
- ActiveTurn,
700
- | "streamedMessageEntryIds"
701
- | "streamedMessageIdentityComplete"
702
- | "lastCompletedAssistantOutcome"
703
- | "lastCompletedAssistantEntryId"
704
- >,
705
- retainedMessages: readonly OmpMessage[],
706
- ): AgentEndOutcome | undefined {
707
- if (
708
- messages.length < declaredCount ||
709
- !turn.streamedMessageIdentityComplete ||
710
- turn.streamedMessageEntryIds.length === 0
711
- ) {
712
- return undefined;
713
- }
714
- const startIndex = messages.length - declaredCount;
715
- let historyIndex = startIndex;
716
- for (const entryId of turn.streamedMessageEntryIds) {
717
- while (historyIndex < messages.length) {
718
- const historyMessage = messages[historyIndex];
719
- if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
720
- historyIndex += 1;
721
- }
722
- if (historyIndex >= messages.length) return undefined;
723
- const correlated = messages[historyIndex];
724
- if (
725
- entryId === turn.lastCompletedAssistantEntryId &&
726
- (correlated?.role !== "assistant" ||
727
- assistantTerminalOutcome(correlated) !== turn.lastCompletedAssistantOutcome)
728
- )
729
- return undefined;
730
- historyIndex += 1;
731
- }
732
- historyIndex = startIndex;
733
- for (const retained of retainedMessages) {
734
- const entryId = nativeEntryId(retained);
735
- if (!entryId) return undefined;
736
- while (historyIndex < messages.length) {
737
- const historyMessage = messages[historyIndex];
738
- if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
739
- historyIndex += 1;
740
- }
741
- if (historyIndex >= messages.length) return undefined;
742
- const correlated = messages[historyIndex];
743
- if (!correlated || correlated.role !== retained.role) return undefined;
744
- if (
745
- retained.role === "assistant" &&
746
- correlated.role === "assistant" &&
747
- assistantTerminalOutcome(retained) !== assistantTerminalOutcome(correlated)
748
- )
749
- return undefined;
750
- historyIndex += 1;
751
- }
752
- const status = lastAssistantStatus(messages, startIndex);
753
- return status === "unavailable" ? undefined : status;
754
- }
755
-
756
- function unknownTerminalOutcomeError(
757
- event: Extract<OmpRpcEvent, { type: "agent_end" }>,
758
- turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
759
- ): string {
760
- const retainedMessages = event.messages?.length ?? 0;
761
- const retainedStatus = event.messages ? lastAssistantStatus(event.messages) : "unavailable";
762
- const lastStatus =
763
- retainedStatus === "unavailable"
764
- ? (turn.lastCompletedAssistantOutcome ?? "unavailable")
765
- : retainedStatus;
766
- return (
767
- "OMP agent_end omitted terminal messages; outcome is unknown " +
768
- `(declaredCount=${event.messageCount ?? "unavailable"}, ` +
769
- `observedCount=${turn.completedMessageCount}, ` +
770
- `retainedTerminalMessages=${retainedMessages}, lastAssistantStatus=${lastStatus})`
771
- );
772
- }
773
-
774
- function isNativeTurnActivity(event: OmpRpcEvent): boolean {
775
- if (
776
- event.type === "agent_start" ||
777
- event.type === "turn_start" ||
778
- event.type === "turn_end" ||
779
- event.type === "agent_end" ||
780
- event.type === "auto_compaction_start" ||
781
- event.type === "auto_compaction_end"
782
- ) {
783
- return true;
784
- }
785
- if (
786
- event.type === "message_start" ||
787
- event.type === "message_update" ||
788
- event.type === "message_end"
789
- ) {
790
- return event.message.role === "assistant";
791
- }
792
- return event.type.startsWith("tool_execution_");
793
- }
794
- function isRuntimeConfigEvent(event: OmpRpcEvent): boolean {
795
- return (
796
- event.type === "model_changed" ||
797
- event.type === "thinking_level_changed" ||
798
- event.type === "retry_fallback_applied" ||
799
- event.type === "retry_fallback_succeeded"
800
- );
801
- }
802
-
803
- function isPassiveUiMethod(method: string): boolean {
804
- return (
805
- method === "cancel" ||
806
- method === "notify" ||
807
- method === "open_url" ||
808
- method === "setStatus" ||
809
- method === "setWidget" ||
810
- method === "setTitle" ||
811
- method === "set_editor_text"
812
- );
813
- }
814
- function createActiveTurn(
815
- clientMessageId: string,
816
- text: string,
817
- generation: number,
818
- terminalPolicy: TerminalCorrelation["policy"],
819
- manualCompaction = false,
820
- ): ActiveTurn {
821
- return {
822
- turnId: randomUUID(),
823
- clientMessageId,
824
- agentInvoked: undefined,
825
- generation,
826
- promptResultEmitted: false,
827
- started: false,
828
- terminal: false,
829
- awaitingPermissionEvidence: false,
830
- interrupted: false,
831
- starting: true,
832
- nativeActivity: false,
833
- userEchoObserved: false,
834
- localOnlyDisabled: false,
835
- localOnlyEligible: false,
836
- usageSampleFloor: 0,
837
- agentEndPending: false,
838
- terminalizing: false,
839
- manualCompactionPending: manualCompaction,
840
- manualCompaction,
841
- activitySequence: 0,
842
- acknowledged: false,
843
- terminalCorrelation: { policy: terminalPolicy, evidence: new Map() },
844
- replayingBufferedEvents: false,
845
- steersInFlight: 0,
846
- activeToolCallIds: new Set(),
847
- steerReady: Promise.withResolvers<void>(),
848
- userCorrelationActive: false,
849
- userLookups: new Set(),
850
- userEchoes: [],
851
- completedMessageCount: 0,
852
- bufferedEvents: [],
853
- streamedMessageEntryIds: [],
854
- streamedMessageIdentityComplete: true,
855
- pendingUsers: [
856
- {
857
- clientMessageId,
858
- text,
859
- accepted: true,
860
- fallbackOnFinish: true,
861
- bufferedEchoes: [],
862
- },
863
- ],
864
- };
865
- }
866
- function classifyTerminalCandidate(
867
- turn: ActiveTurn,
868
- event: Extract<OmpRpcEvent, { type: "agent_end" }>,
869
- arrivalSequence = turn.activitySequence,
870
- ): TerminalCandidate {
871
- if (event.requestId !== undefined) return { event, confidence: "keyed", arrivalSequence };
872
- if (turn.agentInvoked === false) return { event, confidence: "ambiguous", arrivalSequence };
873
- if (turn.terminalCorrelation.policy === "initial-turn") {
874
- return { event, confidence: "initial-turn", arrivalSequence };
875
- }
876
- const assistantSequence = turn.terminalCorrelation.evidence.get("current-assistant");
877
- if (
878
- turn.terminalCorrelation.policy === "native-command" &&
879
- assistantSequence !== undefined &&
880
- assistantSequence <= arrivalSequence
881
- ) {
882
- return { event, confidence: "native-command", arrivalSequence };
883
- }
884
- const userSequence = turn.terminalCorrelation.evidence.get("fresh-native-user");
885
- if (
886
- userSequence !== undefined &&
887
- assistantSequence !== undefined &&
888
- userSequence < assistantSequence &&
889
- assistantSequence <= arrivalSequence
890
- ) {
891
- return { event, confidence: "ordered-legacy", arrivalSequence };
892
- }
893
- return { event, confidence: "ambiguous", arrivalSequence };
894
- }
895
-
896
274
  export class OmpProviderSession {
897
275
  readonly id: string;
898
276
  readonly cwd: string;
@@ -928,30 +306,12 @@ export class OmpProviderSession {
928
306
  private configRevision = 0;
929
307
  private recoveryUsesNativeConfig = false;
930
308
  private activeAbort: PendingAbort | null = null;
931
- private usageEpoch = 0;
932
- private usageSequence = 0;
933
- private usageSample: {
934
- turn: ActiveTurn;
935
- generation: number;
936
- runtime: OmpRuntimeSession;
937
- epoch: number;
938
- sequence: number;
939
- promise: Promise<OmpSessionState | undefined>;
940
- } | null = null;
941
- private activeCompaction: ActiveCompaction | null = null;
942
- private discardedCompactionEnds = 0;
943
- private lastUsage: ProviderUsage | null = null;
309
+ private readonly usage: OmpSessionUsage;
310
+ private readonly compaction: OmpSessionCompaction;
944
311
  private revertInFlight = false;
945
312
  private runtimeTurnCompleted = false;
946
313
  private commandCatalog: OmpAvailableCommand[];
947
- private permissionSequence = 0;
948
- private readonly permissionNamespace = randomUUID();
949
- private readonly pendingPermissions = new Map<string, PendingPermission>();
950
- private readonly inFlightPermissions = new Map<string, PendingPermission>();
951
- private pendingFreeformSelection: PendingFreeformSelection | null = null;
952
- private readonly pendingToolPermissions = new Map<string, PendingToolPermission>();
953
- private readonly inFlightToolPermissions = new Map<string, PendingToolPermission>();
954
- private readonly resolvedToolApprovalIds = new BoundedStringSet(MAX_TRACKED_ENTRY_IDS);
314
+ private readonly permissions: OmpSessionPermissions;
955
315
  private unsupportedThinkingNoticePending = false;
956
316
  private unsupportedThinkingNoticePublished = false;
957
317
 
@@ -984,6 +344,26 @@ export class OmpProviderSession {
984
344
  this.id = id;
985
345
  this.cwd = config.cwd;
986
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
+ );
987
367
  this.dataFilter = new OmpPublicDataSerializer(outputRedactionValues);
988
368
  this.nativeModelsByPublicId = nativeModelsByPublicId;
989
369
  this.commandCatalog = commandCatalog;
@@ -997,6 +377,31 @@ export class OmpProviderSession {
997
377
  capabilities.includes("timeline.plugin"),
998
378
  hostTools.labels,
999
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
+ });
1000
405
  this.subsessions = capabilities.includes("session.subsession")
1001
406
  ? new OmpSubsessionProjector(
1002
407
  id,
@@ -1046,6 +451,7 @@ export class OmpProviderSession {
1046
451
  mcpConnector?: OmpMcpConnector,
1047
452
  mcpInitializationTimeoutMs?: number,
1048
453
  reportOperationalFailure: OmpOperationalFailureReporter = () => {},
454
+ hostInheritEnv: readonly string[] = [],
1049
455
  ): Promise<OmpProviderSession> {
1050
456
  const resumeSessionId = ompPersistenceSessionId(input);
1051
457
  if (resumeSessionId && !input.config.persist) {
@@ -1061,6 +467,7 @@ export class OmpProviderSession {
1061
467
  const normalizedConfig = normalizeOmpSessionConfig(
1062
468
  effectiveConfig,
1063
469
  capabilities.includes("permission"),
470
+ hostInheritEnv,
1064
471
  );
1065
472
  const configuredRedactionValues = configuredOutputRedactionValues(
1066
473
  normalizedConfig.outputRedaction ?? "none",
@@ -1338,289 +745,6 @@ export class OmpProviderSession {
1338
745
  });
1339
746
  }
1340
747
 
1341
- private usageFrom(
1342
- state: OmpSessionState | undefined,
1343
- stats: OmpSessionStats | undefined,
1344
- ): ProviderUsage | undefined {
1345
- if (!state?.contextUsage && !stats) return undefined;
1346
- const stateContext = state?.contextUsage;
1347
- const statsContext = stats?.contextUsage;
1348
- const modelCapacity = state?.model?.contextWindow;
1349
- const inputTokens = stats?.tokens?.input;
1350
- const cachedInputTokens = stats?.tokens?.cacheRead;
1351
- const outputTokens = stats?.tokens?.output;
1352
- const totalCostUsd = stats?.cost;
1353
- const contextTokens =
1354
- typeof stateContext?.tokens === "number"
1355
- ? stateContext.tokens
1356
- : typeof statsContext?.tokens === "number"
1357
- ? statsContext.tokens
1358
- : undefined;
1359
- const contextWindow =
1360
- typeof stateContext?.contextWindow === "number" && stateContext.contextWindow > 0
1361
- ? stateContext.contextWindow
1362
- : typeof statsContext?.contextWindow === "number" && statsContext.contextWindow > 0
1363
- ? statsContext.contextWindow
1364
- : typeof modelCapacity === "number" && modelCapacity > 0
1365
- ? modelCapacity
1366
- : undefined;
1367
- const usage: ProviderUsage = {
1368
- ...(typeof inputTokens === "number" ? { inputTokens } : {}),
1369
- ...(typeof cachedInputTokens === "number" ? { cachedInputTokens } : {}),
1370
- ...(typeof outputTokens === "number" ? { outputTokens } : {}),
1371
- ...(typeof totalCostUsd === "number" ? { totalCostUsd } : {}),
1372
- ...(contextTokens !== undefined ? { contextWindowUsedTokens: contextTokens } : {}),
1373
- ...(contextWindow !== undefined ? { contextWindowMaxTokens: contextWindow } : {}),
1374
- };
1375
- return Object.keys(usage).length > 0 ? usage : undefined;
1376
- }
1377
-
1378
- private ownsUsageSample(
1379
- turn: ActiveTurn,
1380
- generation: number,
1381
- runtime: OmpRuntimeSession,
1382
- ): boolean {
1383
- return (
1384
- !this.closed &&
1385
- !this.runtimeDead &&
1386
- !turn.terminal &&
1387
- generation === this.generation &&
1388
- runtime === this.runtime &&
1389
- this.activeTurn === turn
1390
- );
1391
- }
1392
-
1393
- private publishUsageSnapshot(
1394
- turn: ActiveTurn,
1395
- minimumEpoch = this.usageEpoch,
1396
- minimumSequence = turn.usageSampleFloor,
1397
- ): Promise<OmpSessionState | undefined> {
1398
- const generation = turn.generation;
1399
- const runtime = this.runtime;
1400
- if (!this.ownsUsageSample(turn, generation, runtime)) return Promise.resolve(undefined);
1401
- const current = this.usageSample;
1402
- if (current) {
1403
- if (
1404
- current.turn === turn &&
1405
- current.generation === generation &&
1406
- current.runtime === runtime &&
1407
- current.epoch >= minimumEpoch &&
1408
- current.sequence >= minimumSequence
1409
- ) {
1410
- return current.promise;
1411
- }
1412
- return current.promise.then(() => {
1413
- if (!this.ownsUsageSample(turn, generation, runtime)) return undefined;
1414
- return this.publishUsageSnapshot(turn, minimumEpoch, minimumSequence);
1415
- });
1416
- }
1417
- const epoch = this.usageEpoch;
1418
- const sequence = ++this.usageSequence;
1419
- const promise = Promise.allSettled([runtime.getState(), runtime.getSessionStats()]).then(
1420
- ([stateResult, statsResult]) => {
1421
- if (
1422
- !this.ownsUsageSample(turn, generation, runtime) ||
1423
- epoch !== this.usageEpoch ||
1424
- epoch < minimumEpoch ||
1425
- sequence < turn.usageSampleFloor ||
1426
- sequence < minimumSequence
1427
- ) {
1428
- return undefined;
1429
- }
1430
- const state = stateResult.status === "fulfilled" ? stateResult.value : undefined;
1431
- const stats = statsResult.status === "fulfilled" ? statsResult.value : undefined;
1432
- const usage = this.usageFrom(state, stats);
1433
- if (usage) {
1434
- this.lastUsage = usage;
1435
- this.emit({ type: "session.usage", sessionId: this.id, turnId: turn.turnId, usage });
1436
- }
1437
- return state;
1438
- },
1439
- );
1440
- this.usageSample = { turn, generation, runtime, epoch, sequence, promise };
1441
- void promise.finally(() => {
1442
- if (this.usageSample?.promise === promise) this.usageSample = null;
1443
- });
1444
- return promise;
1445
- }
1446
-
1447
- private async boundedUsageSnapshot(
1448
- turn: ActiveTurn,
1449
- timeoutMs: number,
1450
- ): Promise<OmpSessionState | undefined> {
1451
- const timeout = Promise.withResolvers<undefined>();
1452
- const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
1453
- try {
1454
- return await Promise.race([this.publishUsageSnapshot(turn), timeout.promise]);
1455
- } finally {
1456
- this.scheduler.clear(timer);
1457
- }
1458
- }
1459
-
1460
- private async boundedTerminalState(
1461
- turn: ActiveTurn,
1462
- timeoutMs: number,
1463
- ): Promise<OmpSessionState | undefined> {
1464
- const generation = turn.generation;
1465
- const runtime = this.runtime;
1466
- const timeout = Promise.withResolvers<undefined>();
1467
- const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
1468
- try {
1469
- const state = await Promise.race([runtime.getState(), timeout.promise]);
1470
- return this.ownsUsageSample(turn, generation, runtime) ? state : undefined;
1471
- } catch {
1472
- return undefined;
1473
- } finally {
1474
- this.scheduler.clear(timer);
1475
- }
1476
- }
1477
-
1478
- private isActiveTurn(turn: ActiveTurn): boolean {
1479
- return (
1480
- !this.closed &&
1481
- !this.runtimeDead &&
1482
- !turn.terminal &&
1483
- !turn.terminalizing &&
1484
- !turn.agentEndPending &&
1485
- turn.generation === this.generation &&
1486
- this.activeTurn === turn
1487
- );
1488
- }
1489
-
1490
- private scheduleUsagePoll(turn: ActiveTurn, delayMs = USAGE_POLL_MS): void {
1491
- if (!this.isActiveTurn(turn) || turn.usagePollTimer !== undefined) return;
1492
- turn.usagePollTimer = this.scheduler.set(() => {
1493
- turn.usagePollTimer = undefined;
1494
- this.pollUsage(turn);
1495
- }, delayMs);
1496
- }
1497
-
1498
- private pollUsage(turn: ActiveTurn): void {
1499
- if (!this.isActiveTurn(turn) || turn.usagePoll) return;
1500
- const poll = this.publishUsageSnapshot(turn).then(() => undefined);
1501
- turn.usagePoll = poll;
1502
- void poll.finally(() => {
1503
- if (turn.usagePoll === poll) turn.usagePoll = undefined;
1504
- this.scheduleUsagePoll(turn);
1505
- });
1506
- }
1507
-
1508
- private stopUsagePoll(turn: ActiveTurn): void {
1509
- if (turn.usagePollTimer === undefined) return;
1510
- this.scheduler.clear(turn.usagePollTimer);
1511
- turn.usagePollTimer = undefined;
1512
- }
1513
-
1514
- private startCompaction(turn: ActiveTurn, trigger: "auto" | "manual", action?: string): void {
1515
- if (this.discardedCompactionEnds > 0) {
1516
- this.discardedCompactionEnds += 1;
1517
- return;
1518
- }
1519
- const active = this.activeCompaction;
1520
- if (active && active.trigger === trigger && active.action === action) {
1521
- active.retrying = false;
1522
- return;
1523
- }
1524
- if (active) {
1525
- this.retireCompaction("OMP emitted overlapping compactions");
1526
- this.discardedCompactionEnds = 2;
1527
- return;
1528
- }
1529
- const operation: ActiveCompaction = {
1530
- id: randomUUID(),
1531
- trigger,
1532
- turnId: turn.turnId,
1533
- generation: turn.generation,
1534
- action,
1535
- retrying: false,
1536
- preTokens: this.lastUsage?.contextWindowUsedTokens,
1537
- };
1538
- this.activeCompaction = operation;
1539
- this.projector.flush(true);
1540
- this.emit({
1541
- type: "timeline.item",
1542
- sessionId: this.id,
1543
- item: {
1544
- id: operation.id,
1545
- type: "compaction",
1546
- status: "loading",
1547
- trigger,
1548
- ...(operation.preTokens !== undefined ? { preTokens: operation.preTokens } : {}),
1549
- },
1550
- });
1551
- }
1552
-
1553
- private finishCompaction(
1554
- state: "completed" | "failed" | "canceled" | "skipped",
1555
- options: { tokensBefore?: number | null; message?: string } = {},
1556
- ): void {
1557
- const operation = this.activeCompaction;
1558
- if (!operation) return;
1559
- this.activeCompaction = null;
1560
- this.projector.flush(true);
1561
- if (state === "completed") {
1562
- this.usageEpoch += 1;
1563
- const staleSample = this.usageSample;
1564
- if (staleSample && staleSample.epoch < this.usageEpoch) {
1565
- this.usageSample = null;
1566
- staleSample.turn.usagePoll = undefined;
1567
- }
1568
- this.lastUsage = null;
1569
- }
1570
- if (state !== "completed") {
1571
- const defaultMessage =
1572
- state === "failed"
1573
- ? "OMP compaction failed"
1574
- : state === "canceled"
1575
- ? "OMP compaction canceled"
1576
- : "OMP compaction skipped";
1577
- this.emit({
1578
- type: "timeline.item",
1579
- sessionId: this.id,
1580
- item: {
1581
- id: operation.id,
1582
- type: "notification",
1583
- level: state === "failed" ? "error" : "info",
1584
- message: this.dataFilter.text(options.message ?? defaultMessage, 4_096),
1585
- },
1586
- });
1587
- return;
1588
- }
1589
- const tokensBefore = options.tokensBefore ?? operation.preTokens;
1590
- this.emit({
1591
- type: "timeline.item",
1592
- sessionId: this.id,
1593
- item: {
1594
- id: operation.id,
1595
- type: "compaction",
1596
- status: "completed",
1597
- trigger: operation.trigger,
1598
- ...(tokensBefore !== undefined ? { preTokens: tokensBefore } : {}),
1599
- },
1600
- });
1601
- }
1602
-
1603
- private retireCompaction(message: string): void {
1604
- const operation = this.activeCompaction;
1605
- if (!operation) return;
1606
- this.activeCompaction = null;
1607
- this.emit({
1608
- type: "timeline.item",
1609
- sessionId: this.id,
1610
- item: {
1611
- id: operation.id,
1612
- type: "compaction",
1613
- status: "completed",
1614
- trigger: operation.trigger,
1615
- },
1616
- });
1617
- this.emit({
1618
- type: "timeline.item",
1619
- sessionId: this.id,
1620
- item: { id: `${operation.id}:error`, type: "error", message },
1621
- });
1622
- }
1623
-
1624
748
  private async replayHistory(preferPersistedTranscript = false): Promise<void> {
1625
749
  if (!this.runtime.canReplayHistory) {
1626
750
  throw new OmpPublicError("OMP session history cannot be replayed safely");
@@ -1685,7 +809,7 @@ export class OmpProviderSession {
1685
809
  });
1686
810
  }
1687
811
  }
1688
- messages ??= await waitForReplay(this.runtime.getMessages(), replay.signal);
812
+ messages ??= await waitForReplay(this.runtime.getMessages(replay.signal), replay.signal);
1689
813
  this.quarantineBranchEntries();
1690
814
  for (const message of messages) {
1691
815
  replay.signal.throwIfAborted();
@@ -1858,7 +982,7 @@ export class OmpProviderSession {
1858
982
  const configRefresh = this.configRefreshInFlight;
1859
983
  this.cancelConfigRefreshRetry();
1860
984
  this.lifetime.abort(new Error(message));
1861
- this.resolveAllPermissions(true);
985
+ this.permissions.resolveAllPermissions(true);
1862
986
  this.subsessions?.close();
1863
987
  this.projector.close();
1864
988
  this.unsubscribe();
@@ -2013,8 +1137,8 @@ export class OmpProviderSession {
2013
1137
  turn.starting = false;
2014
1138
  turn.acknowledged = true;
2015
1139
  this.startTurn(turn, false);
2016
- this.startCompaction(turn, "manual");
2017
- this.pollUsage(turn);
1140
+ this.compaction.start(turn, "manual");
1141
+ this.usage.poll(turn);
2018
1142
  const bufferedEvents = turn.bufferedEvents.splice(0);
2019
1143
  turn.replayingBufferedEvents = true;
2020
1144
  for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
@@ -2105,8 +1229,8 @@ export class OmpProviderSession {
2105
1229
  else {
2106
1230
  turn.steerReady.resolve();
2107
1231
  turn.terminal = true;
2108
- this.finishCompaction("failed", { message: "OMP prompt failed" });
2109
- this.resolveTurnPermissions(turn.turnId);
1232
+ this.compaction.finish("failed", { message: "OMP prompt failed" });
1233
+ this.permissions.resolveTurnPermissions(turn.turnId);
2110
1234
  this.projector.finishTurn(turn.turnId);
2111
1235
  if (this.activeTurn === turn) this.activeTurn = null;
2112
1236
  }
@@ -2278,7 +1402,7 @@ export class OmpProviderSession {
2278
1402
  const result = await compaction;
2279
1403
  turn.manualCompactionPending = false;
2280
1404
  if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
2281
- this.finishCompaction("completed", { tokensBefore: result.tokensBefore });
1405
+ this.compaction.finish("completed", { tokensBefore: result.tokensBefore });
2282
1406
  await this.finishTurn(turn, turn.interrupted ? "canceled" : "completed");
2283
1407
  } catch (error) {
2284
1408
  if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
@@ -2286,7 +1410,7 @@ export class OmpProviderSession {
2286
1410
  const raw = providerError(error, "OMP compaction failed");
2287
1411
  const failure = { message: this.dataFilter.text(raw.message, 4_096) };
2288
1412
  const state = turn.interrupted ? "canceled" : "failed";
2289
- this.finishCompaction(state, { message: failure.message });
1413
+ this.compaction.finish(state, { message: failure.message });
2290
1414
  await this.finishTurn(turn, state, state === "failed" ? failure : undefined, true, true);
2291
1415
  }
2292
1416
  }
@@ -2307,7 +1431,7 @@ export class OmpProviderSession {
2307
1431
  }
2308
1432
  const forceTerminal = turn.awaitingPermissionEvidence;
2309
1433
  turn.awaitingPermissionEvidence = false;
2310
- if (forceTerminal) this.resolveTurnPermissions(turn.turnId);
1434
+ if (forceTerminal) this.permissions.resolveTurnPermissions(turn.turnId);
2311
1435
  const pending = this.activeAbort;
2312
1436
  if (turn.interrupted) {
2313
1437
  if (pending?.turn === turn) await this.settleInterrupt(input.requestId, pending);
@@ -2718,118 +1842,7 @@ export class OmpProviderSession {
2718
1842
  }
2719
1843
 
2720
1844
  async permission(input: SessionPermissionInput): Promise<void> {
2721
- const typed = this.pendingToolPermissions.get(input.permissionId);
2722
- if (typed) {
2723
- await this.respondToToolPermission(input, typed);
2724
- return;
2725
- }
2726
- const pending = this.pendingPermissions.get(input.permissionId);
2727
- if (!pending) throw new OmpPublicError("Unknown OMP permission request");
2728
- if (!this.permissionOwnerIsCurrent(pending)) {
2729
- this.pendingPermissions.delete(input.permissionId);
2730
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
2731
- this.emit({
2732
- type: "session.permission_resolved",
2733
- sessionId: this.id,
2734
- permissionId: input.permissionId,
2735
- });
2736
- throw new OmpPublicError("OMP permission request is no longer active");
2737
- }
2738
- const { generation, runtime } = pending;
2739
- this.pendingPermissions.delete(input.permissionId);
2740
- this.inFlightPermissions.set(input.permissionId, pending);
2741
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
2742
- try {
2743
- const freeformValue = this.freeformSelection(pending, input.response);
2744
- if (freeformValue !== undefined) {
2745
- this.pendingFreeformSelection = {
2746
- value: freeformValue,
2747
- nativeSelectId: pending.nativeId,
2748
- generation,
2749
- runtime,
2750
- ...(pending.turnId ? { turnId: pending.turnId } : {}),
2751
- };
2752
- }
2753
- const response = this.extensionUiResponse(pending, input.response);
2754
- await runtime.respondToExtensionUi(response);
2755
- } catch (error) {
2756
- if (this.pendingFreeformSelection?.nativeSelectId === pending.nativeId) {
2757
- this.pendingFreeformSelection = null;
2758
- }
2759
- if (this.inFlightPermissions.get(input.permissionId) !== pending) return;
2760
- this.inFlightPermissions.delete(input.permissionId);
2761
- if (
2762
- this.permissionOwnerIsCurrent(pending) &&
2763
- generation === this.generation &&
2764
- runtime === this.runtime
2765
- ) {
2766
- this.pendingPermissions.set(input.permissionId, pending);
2767
- this.armPermissionTimeout(input.permissionId, pending);
2768
- throw error;
2769
- }
2770
- return;
2771
- }
2772
- if (this.inFlightPermissions.get(input.permissionId) !== pending) return;
2773
- this.inFlightPermissions.delete(input.permissionId);
2774
- this.emit({
2775
- type: "session.permission_resolved",
2776
- sessionId: this.id,
2777
- permissionId: input.permissionId,
2778
- });
2779
- this.reevaluateDeferredPermissionTerminal();
2780
- }
2781
- private async respondToToolPermission(
2782
- input: SessionPermissionInput,
2783
- pending: PendingToolPermission,
2784
- ): Promise<void> {
2785
- if (!this.permissionOwnerIsCurrent(pending)) {
2786
- this.pendingToolPermissions.delete(input.permissionId);
2787
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
2788
- this.resolvedToolApprovalIds.add(pending.nativeId);
2789
- this.emit({
2790
- type: "session.permission_resolved",
2791
- sessionId: this.id,
2792
- permissionId: input.permissionId,
2793
- });
2794
- throw new OmpPublicError("OMP permission request is no longer active");
2795
- }
2796
- if (
2797
- input.response.selectedActionId !== undefined &&
2798
- input.response.selectedActionId !== input.response.behavior
2799
- ) {
2800
- throw new OmpPublicError("OMP permission action is invalid");
2801
- }
2802
- this.pendingToolPermissions.delete(input.permissionId);
2803
- this.inFlightToolPermissions.set(input.permissionId, pending);
2804
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
2805
- try {
2806
- await pending.runtime.respondToToolApproval({
2807
- type: "tool_approval_response",
2808
- id: pending.nativeId,
2809
- toolCallId: pending.toolCallId,
2810
- approved: input.response.behavior === "allow",
2811
- });
2812
- } catch (error) {
2813
- if (this.inFlightToolPermissions.get(input.permissionId) !== pending) return;
2814
- this.inFlightToolPermissions.delete(input.permissionId);
2815
- this.resolvedToolApprovalIds.add(pending.nativeId);
2816
- this.emit({
2817
- type: "session.permission_resolved",
2818
- sessionId: this.id,
2819
- permissionId: input.permissionId,
2820
- });
2821
- this.reevaluateDeferredPermissionTerminal();
2822
- throw error;
2823
- }
2824
- if (this.inFlightToolPermissions.get(input.permissionId) !== pending) return;
2825
- this.inFlightToolPermissions.delete(input.permissionId);
2826
- this.resolvedToolApprovalIds.add(pending.nativeId);
2827
- this.emit({
2828
- type: "session.permission_resolved",
2829
- sessionId: this.id,
2830
- permissionId: input.permissionId,
2831
- });
2832
- this.reevaluateDeferredPermissionTerminal();
1845
+ await this.permissions.respond(input);
2833
1846
  }
2834
1847
 
2835
1848
  close(input?: SessionCloseInput): Promise<void> {
@@ -2863,12 +1876,12 @@ export class OmpProviderSession {
2863
1876
  if (turn.started) await this.finishTurn(turn, "canceled", undefined, true, true);
2864
1877
  else {
2865
1878
  this.settleUnstartedTurn(turn);
2866
- this.finishCompaction("canceled");
1879
+ this.compaction.finish("canceled");
2867
1880
  }
2868
1881
  }
2869
1882
  this.imageMaterializer.clear();
2870
- this.finishCompaction("canceled");
2871
- this.resolveAllPermissions(true);
1883
+ this.compaction.finish("canceled");
1884
+ this.permissions.resolveAllPermissions(true);
2872
1885
  this.closed = true;
2873
1886
  this.configRefreshAttempts = 0;
2874
1887
  this.configRefreshDirty = false;
@@ -3027,7 +2040,7 @@ export class OmpProviderSession {
3027
2040
  }
3028
2041
  if (this.subsessions) await recovered.setSubagentSubscription("events");
3029
2042
  this.generation += 1;
3030
- this.lastUsage = null;
2043
+ this.usage.clearLatest();
3031
2044
  this.runtimeDead = null;
3032
2045
  this.runtimeDisposal = null;
3033
2046
  unsubscribeBootstrap();
@@ -3193,7 +2206,7 @@ export class OmpProviderSession {
3193
2206
  this.handleRuntimeFailure("OMP emitted an unnegotiated tool approval request");
3194
2207
  return;
3195
2208
  }
3196
- this.publishToolPermission(event);
2209
+ this.permissions.publishToolPermission(event);
3197
2210
  return;
3198
2211
  }
3199
2212
  if (event.type === "tool_approval_cancel") {
@@ -3201,18 +2214,17 @@ export class OmpProviderSession {
3201
2214
  this.handleRuntimeFailure("OMP emitted an unnegotiated tool approval cancellation");
3202
2215
  return;
3203
2216
  }
3204
- this.cancelToolPermission(event);
2217
+ this.permissions.cancelToolPermission(event);
3205
2218
  return;
3206
2219
  }
3207
2220
  if (event.type === "extension_ui_request") {
3208
2221
  if (event.method === "cancel") {
3209
- if (this.pendingFreeformSelection?.nativeSelectId === event.targetId) {
3210
- this.pendingFreeformSelection = null;
3211
- }
3212
- this.resolvePermissionByNativeId(event.targetId ?? "");
2222
+ this.permissions.clearPendingFreeformSelection(event.targetId);
2223
+ this.permissions.resolvePermissionByNativeId(event.targetId ?? "");
3213
2224
  return;
3214
2225
  }
3215
- if (event.method === "input" && this.submitPendingFreeformSelection(event)) return;
2226
+ if (event.method === "input" && this.permissions.submitPendingFreeformSelection(event))
2227
+ return;
3216
2228
  if (
3217
2229
  event.method === "select" ||
3218
2230
  event.method === "confirm" ||
@@ -3223,7 +2235,7 @@ export class OmpProviderSession {
3223
2235
  this.handleRuntimeFailure();
3224
2236
  return;
3225
2237
  }
3226
- this.publishPermission(event);
2238
+ this.permissions.publishPermission(event);
3227
2239
  return;
3228
2240
  }
3229
2241
 
@@ -3234,6 +2246,17 @@ export class OmpProviderSession {
3234
2246
  this.handleRuntimeFailure();
3235
2247
  return;
3236
2248
  }
2249
+ if (
2250
+ event.type === "config_warnings_changed" ||
2251
+ event.type === "advisor_cost_changed" ||
2252
+ event.type === "ttsr_triggered"
2253
+ ) {
2254
+ return;
2255
+ }
2256
+ if (event.type === "irc_message") {
2257
+ this.projector.projectPassive(event);
2258
+ return;
2259
+ }
3237
2260
  if (
3238
2261
  event.type === "notice" ||
3239
2262
  event.type === "todo_reminder" ||
@@ -3363,50 +2386,20 @@ export class OmpProviderSession {
3363
2386
  if (event.type === "auto_compaction_start") {
3364
2387
  turn.nativeActivity = true;
3365
2388
  this.cancelLocalOnlyCompletion(turn);
3366
- this.startCompaction(turn, "auto", event.action);
2389
+ this.compaction.start(turn, "auto", event.action);
3367
2390
  return;
3368
2391
  }
3369
2392
  if (event.type === "auto_compaction_end") {
3370
- if (this.discardedCompactionEnds > 0) {
3371
- this.discardedCompactionEnds -= 1;
3372
- return;
3373
- }
3374
- const operation = this.activeCompaction;
3375
- if (
3376
- operation?.trigger !== "auto" ||
3377
- operation.turnId !== turn.turnId ||
3378
- operation.generation !== turn.generation
3379
- ) {
3380
- return;
3381
- }
3382
- if (event.action !== undefined && operation.action !== event.action) {
3383
- this.retireCompaction("OMP emitted overlapping compactions");
3384
- this.discardedCompactionEnds = 1;
3385
- return;
2393
+ if (this.compaction.handleAutoEnd(turn, event)) {
2394
+ this.usage.schedulePoll(turn, USAGE_REFRESH_MS);
3386
2395
  }
3387
- if (event.willRetry) {
3388
- operation.retrying = true;
3389
- return;
3390
- }
3391
- const state = event.aborted
3392
- ? "canceled"
3393
- : event.errorMessage
3394
- ? "failed"
3395
- : event.skipped
3396
- ? "skipped"
3397
- : "completed";
3398
- this.finishCompaction(state, {
3399
- tokensBefore: event.result?.tokensBefore ?? event.result?.preTokens,
3400
- message: event.errorMessage,
3401
- });
3402
- this.scheduleUsagePoll(turn, USAGE_REFRESH_MS);
3403
2396
  return;
3404
2397
  }
3405
2398
  if (event.type === "agent_end" && turn.manualCompactionPending) return;
3406
2399
  if (event.type === "tool_execution_start") turn.activeToolCallIds.add(event.toolCallId);
3407
2400
  if (event.type === "tool_execution_end") {
3408
2401
  turn.activeToolCallIds.delete(event.toolCallId);
3409
- if (event.toolName === "ask_user") this.pendingFreeformSelection = null;
2402
+ if (event.toolName === "ask_user") this.permissions.clearPendingFreeformSelection();
3410
2403
  }
3411
2404
  if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
3412
2405
  try {
@@ -3681,24 +2674,6 @@ export class OmpProviderSession {
3681
2674
  });
3682
2675
  }
3683
2676
 
3684
- private submitPendingFreeformSelection(
3685
- request: Extract<OmpQuestionRequest, { method: "input" }>,
3686
- ): boolean {
3687
- const pending = this.pendingFreeformSelection;
3688
- if (!pending) return false;
3689
- this.pendingFreeformSelection = null;
3690
- if (
3691
- pending.generation !== this.generation ||
3692
- pending.runtime !== this.runtime ||
3693
- (pending.turnId !== undefined && this.activeTurn?.turnId !== pending.turnId)
3694
- ) {
3695
- return false;
3696
- }
3697
- void pending.runtime
3698
- .respondToExtensionUi({ type: "extension_ui_response", id: request.id, value: pending.value })
3699
- .catch(() => this.handleRuntimeFailure("OMP freeform response failed"));
3700
- return true;
3701
- }
3702
2677
  private replaceSlashCommands(commands: OmpAvailableCommand[]): void {
3703
2678
  this.slashCommands.clear();
3704
2679
  for (const command of [...OMP_BUILTIN_COMMANDS, ...commands]) {
@@ -3730,687 +2705,11 @@ export class OmpProviderSession {
3730
2705
  }),
3731
2706
  });
3732
2707
  }
3733
- private toolPermissionDetail(
3734
- request: OmpToolApprovalRequest,
3735
- ): ProviderToolCallDetail | undefined {
3736
- switch (request.identity.kind) {
3737
- case "shell":
3738
- return {
3739
- type: "shell",
3740
- command: this.dataFilter.text(request.identity.command, 24 * 1024),
3741
- };
3742
- case "edit":
3743
- return {
3744
- type: "edit",
3745
- filePath: this.dataFilter.text(request.identity.paths[0] ?? "", 4_096),
3746
- newString: this.dataFilter.text(request.identity.content, 20 * 1024),
3747
- };
3748
- case "write":
3749
- return {
3750
- type: "write",
3751
- filePath: this.dataFilter.text(request.identity.path, 4_096),
3752
- content: this.dataFilter.text(request.identity.content, 20 * 1024),
3753
- };
3754
- case "other":
3755
- return;
3756
- }
3757
- }
3758
-
3759
- private publishToolPermission(request: OmpToolApprovalRequest): void {
3760
- const fingerprint = createHash("sha256").update(JSON.stringify(request)).digest("base64url");
3761
- if (this.resolvedToolApprovalIds.has(request.id)) {
3762
- this.handleRuntimeFailure("OMP reused a resolved tool approval identifier");
3763
- return;
3764
- }
3765
- for (const pending of [
3766
- ...this.pendingToolPermissions.values(),
3767
- ...this.inFlightToolPermissions.values(),
3768
- ]) {
3769
- if (pending.nativeId !== request.id) continue;
3770
- if (pending.fingerprint !== fingerprint || pending.toolCallId !== request.toolCallId) {
3771
- this.handleRuntimeFailure("OMP changed a pending tool approval request");
3772
- }
3773
- return;
3774
- }
3775
- const pendingCount =
3776
- this.pendingPermissions.size +
3777
- this.inFlightPermissions.size +
3778
- this.pendingToolPermissions.size +
3779
- this.inFlightToolPermissions.size;
3780
- if (pendingCount >= MAX_PENDING_PERMISSIONS) {
3781
- this.resolvedToolApprovalIds.add(request.id);
3782
- void this.runtime
3783
- .respondToToolApproval({
3784
- type: "tool_approval_response",
3785
- id: request.id,
3786
- toolCallId: request.toolCallId,
3787
- cancelled: true,
3788
- })
3789
- .catch(() => this.handleRuntimeFailure());
3790
- return;
3791
- }
3792
-
3793
- this.permissionSequence += 1;
3794
- const permissionId = `omp:permission:${this.permissionNamespace}:${this.permissionSequence}`;
3795
- const detail = this.toolPermissionDetail(request);
3796
- const filteredInput = this.dataFilter.json(request.input, 8 * 1024, 32 * 1024);
3797
- const input =
3798
- filteredInput && typeof filteredInput === "object" && !Array.isArray(filteredInput)
3799
- ? filteredInput
3800
- : {};
3801
- const descriptionParts = [
3802
- request.detail.reason,
3803
- ...request.detail.lines,
3804
- ...(request.detail.providerSafetyChecks ?? []),
3805
- ].filter((part): part is string => Boolean(part));
3806
- const publicRequest = {
3807
- id: permissionId,
3808
- name: `omp.${this.dataFilter.text(request.toolName, 256)}`,
3809
- kind: "tool" as const,
3810
- title: `Allow ${this.dataFilter.text(request.toolName, 256)}?`,
3811
- ...(descriptionParts.length > 0
3812
- ? { description: this.dataFilter.text(descriptionParts.join("\n"), 16 * 1024) }
3813
- : {}),
3814
- input: {
3815
- ...input,
3816
- identity: this.dataFilter.json(request.identity, 20 * 1024, 32 * 1024),
3817
- tier: request.tier,
3818
- toolCallId: request.toolCallId,
3819
- },
3820
- ...(detail ? { detail } : {}),
3821
- actions: [
3822
- { id: "allow", label: "Allow", behavior: "allow" as const, variant: "primary" as const },
3823
- { id: "deny", label: "Deny", behavior: "deny" as const, variant: "danger" as const },
3824
- ],
3825
- metadata: {
3826
- tier: request.tier,
3827
- redacted: request.detail.redacted,
3828
- truncated: request.detail.truncated,
3829
- redactedFields: request.detail.redactedFields,
3830
- truncatedFields: request.detail.truncatedFields,
3831
- },
3832
- };
3833
- const pending: PendingToolPermission = {
3834
- nativeId: request.id,
3835
- toolCallId: request.toolCallId,
3836
- fingerprint,
3837
- retainedBytes: boundedJsonBytes(
3838
- publicRequest,
3839
- MAX_PENDING_PERMISSION_BYTES,
3840
- 512,
3841
- MAX_PENDING_PERMISSION_BYTES,
3842
- 4_096,
3843
- ),
3844
- generation: this.generation,
3845
- runtime: this.runtime,
3846
- ...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
3847
- ...(request.timeout !== undefined ? { expiresAt: Date.now() + request.timeout } : {}),
3848
- };
3849
- let retainedBytes = pending.retainedBytes;
3850
- for (const item of this.pendingPermissions.values()) retainedBytes += item.retainedBytes;
3851
- for (const item of this.inFlightPermissions.values()) retainedBytes += item.retainedBytes;
3852
- for (const item of this.pendingToolPermissions.values()) retainedBytes += item.retainedBytes;
3853
- for (const item of this.inFlightToolPermissions.values()) retainedBytes += item.retainedBytes;
3854
- if (
3855
- pending.retainedBytes === Number.POSITIVE_INFINITY ||
3856
- retainedBytes > MAX_PENDING_PERMISSION_BYTES
3857
- ) {
3858
- void this.runtime
3859
- .respondToToolApproval({
3860
- type: "tool_approval_response",
3861
- id: request.id,
3862
- toolCallId: request.toolCallId,
3863
- cancelled: true,
3864
- })
3865
- .catch(() => this.handleRuntimeFailure());
3866
- this.resolvedToolApprovalIds.add(request.id);
3867
- return;
3868
- }
3869
- this.pendingToolPermissions.set(permissionId, pending);
3870
- this.armToolPermissionTimeout(permissionId, pending);
3871
- this.projector.markAskPermissionRendered();
3872
- if (this.activeTurn) {
3873
- this.activeTurn.awaitingPermissionEvidence = true;
3874
- this.markAgentEvidence(this.activeTurn);
3875
- }
3876
- this.emit({ type: "session.permission", sessionId: this.id, request: publicRequest });
3877
- }
3878
-
3879
- private cancelToolPermission(request: OmpToolApprovalCancel): void {
3880
- for (const permissions of [this.pendingToolPermissions, this.inFlightToolPermissions]) {
3881
- for (const [permissionId, pending] of permissions) {
3882
- if (pending.nativeId !== request.targetId) continue;
3883
- if (pending.toolCallId !== request.toolCallId) {
3884
- this.handleRuntimeFailure("OMP tool approval cancellation did not match its tool call");
3885
- return;
3886
- }
3887
- permissions.delete(permissionId);
3888
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
3889
- this.resolvedToolApprovalIds.add(pending.nativeId);
3890
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
3891
- this.reevaluateDeferredPermissionTerminal();
3892
- return;
3893
- }
3894
- }
3895
- if (!this.resolvedToolApprovalIds.has(request.targetId)) {
3896
- this.handleRuntimeFailure("OMP canceled an unknown tool approval request");
3897
- }
3898
- }
3899
-
3900
- private publishPermission(request: OmpQuestionRequest): void {
3901
- if (request.method === "select" && !request.options?.length) {
3902
- this.handleRuntimeFailure();
3903
- return;
3904
- }
3905
- const fingerprint = permissionFingerprint(request);
3906
- let existingId: string | undefined;
3907
- let existingPending: PendingPermission | undefined;
3908
- for (const [permissionId, pending] of this.pendingPermissions) {
3909
- if (pending.nativeId !== request.id) continue;
3910
- if (pending.fingerprint === fingerprint) {
3911
- existingId = permissionId;
3912
- existingPending = pending;
3913
- } else {
3914
- this.pendingPermissions.delete(permissionId);
3915
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
3916
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
3917
- }
3918
- break;
3919
- }
3920
- if (!existingId) {
3921
- for (const pending of this.inFlightPermissions.values()) {
3922
- if (pending.nativeId !== request.id) continue;
3923
- if (pending.fingerprint !== fingerprint) this.handleRuntimeFailure();
3924
- return;
3925
- }
3926
- }
3927
- if (
3928
- !existingId &&
3929
- this.pendingPermissions.size +
3930
- this.inFlightPermissions.size +
3931
- this.pendingToolPermissions.size +
3932
- this.inFlightToolPermissions.size >=
3933
- MAX_PENDING_PERMISSIONS
3934
- ) {
3935
- this.rejectPermissionRequest(request, "Too many OMP questions are already pending");
3936
- return;
3937
- }
3938
- if (existingPending?.timer !== undefined) this.scheduler.clear(existingPending.timer);
3939
- if (!existingId) this.permissionSequence += 1;
3940
- const id =
3941
- existingId ?? `omp:permission:${this.permissionNamespace}:${this.permissionSequence}`;
3942
- const header = this.dataFilter.text(request.title ?? "OMP question", 4_096);
3943
- const freeformSentinel =
3944
- request.method === "select" && request.options.includes(OMP_ASK_USER_FREEFORM_SENTINEL)
3945
- ? OMP_ASK_USER_FREEFORM_SENTINEL
3946
- : undefined;
3947
- const optionValues = new Map<string, string>();
3948
- const displayValues = new Map<string, string>();
3949
- const usedOptionLabels = new Set<string>();
3950
- const optionDetails = request.method === "select" ? request.optionDetails : undefined;
3951
- const selectableOptions =
3952
- request.method === "select"
3953
- ? request.options.flatMap((nativeValue, index) =>
3954
- nativeValue === freeformSentinel ? [] : [{ nativeValue, index }],
3955
- )
3956
- : [];
3957
- const options =
3958
- request.method === "select"
3959
- ? selectableOptions.map(({ nativeValue, index }) => {
3960
- const baseLabel = this.dataFilter.text(nativeValue, 4_096);
3961
- let label = baseLabel;
3962
- let suffix = 2;
3963
- while (usedOptionLabels.has(label)) {
3964
- label = `${baseLabel} (${suffix})`;
3965
- suffix += 1;
3966
- }
3967
- usedOptionLabels.add(label);
3968
- const value = `${id}:option:${index}`;
3969
- optionValues.set(value, nativeValue);
3970
- displayValues.set(label, nativeValue);
3971
- return {
3972
- label,
3973
- value,
3974
- ...(optionDetails?.[index]?.description
3975
- ? {
3976
- description: this.dataFilter.text(
3977
- optionDetails[index]?.description ?? "",
3978
- 16_384,
3979
- ),
3980
- }
3981
- : {}),
3982
- };
3983
- })
3984
- : undefined;
3985
- const questionOptions =
3986
- request.method === "confirm" ? [{ label: "Yes" }, { label: "No" }] : (options ?? []);
3987
- const actions =
3988
- request.method === "select"
3989
- ? [
3990
- ...(options ?? []).map((option) => ({
3991
- id: option.value,
3992
- label: option.label,
3993
- behavior: "allow" as const,
3994
- variant: "secondary" as const,
3995
- })),
3996
- {
3997
- id: "cancel",
3998
- label: "Cancel",
3999
- behavior: "deny" as const,
4000
- variant: "secondary" as const,
4001
- },
4002
- ]
4003
- : [
4004
- {
4005
- id: "submit",
4006
- label: request.method === "confirm" ? "Confirm" : "Submit",
4007
- behavior: "allow" as const,
4008
- variant: "primary" as const,
4009
- },
4010
- {
4011
- id: "cancel",
4012
- label: "Cancel",
4013
- behavior: "deny" as const,
4014
- variant: "secondary" as const,
4015
- },
4016
- ];
4017
- const pending: PendingPermission = {
4018
- nativeId: request.id,
4019
- header,
4020
- fingerprint,
4021
- optionValues,
4022
- actionBehaviors: new Map(actions.map((action) => [action.id, action.behavior])),
4023
- displayValues,
4024
- generation: this.generation,
4025
- runtime: this.runtime,
4026
- request,
4027
- retainedBytes: boundedJsonBytes(
4028
- { request, header, options: questionOptions, actions },
4029
- MAX_PENDING_PERMISSION_BYTES,
4030
- 512,
4031
- MAX_PENDING_PERMISSION_BYTES,
4032
- 4_096,
4033
- ),
4034
- ...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
4035
- ...(request.timeout !== undefined ? { expiresAt: Date.now() + request.timeout } : {}),
4036
- ...(freeformSentinel ? { freeformSentinel } : {}),
4037
- };
4038
- let retainedPermissionBytes = pending.retainedBytes;
4039
- for (const [permissionId, retained] of this.pendingPermissions) {
4040
- if (permissionId !== existingId) retainedPermissionBytes += retained.retainedBytes;
4041
- }
4042
- for (const retained of this.inFlightPermissions.values()) {
4043
- retainedPermissionBytes += retained.retainedBytes;
4044
- }
4045
- for (const retained of this.pendingToolPermissions.values()) {
4046
- retainedPermissionBytes += retained.retainedBytes;
4047
- }
4048
- for (const retained of this.inFlightToolPermissions.values()) {
4049
- retainedPermissionBytes += retained.retainedBytes;
4050
- }
4051
- if (
4052
- pending.retainedBytes === Number.POSITIVE_INFINITY ||
4053
- retainedPermissionBytes > MAX_PENDING_PERMISSION_BYTES
4054
- ) {
4055
- if (existingId) {
4056
- this.pendingPermissions.delete(existingId);
4057
- this.emit({
4058
- type: "session.permission_resolved",
4059
- sessionId: this.id,
4060
- permissionId: existingId,
4061
- });
4062
- }
4063
- this.rejectPermissionRequest(request, "OMP question data exceeded the pending input budget");
4064
- return;
4065
- }
4066
- this.pendingPermissions.set(id, pending);
4067
- this.armPermissionTimeout(id, pending);
4068
- this.projector.markAskPermissionRendered();
4069
- if (this.activeTurn) {
4070
- this.activeTurn.awaitingPermissionEvidence = true;
4071
- this.markAgentEvidence(this.activeTurn);
4072
- }
4073
- this.emit({
4074
- type: "session.permission",
4075
- sessionId: this.id,
4076
- request: {
4077
- id,
4078
- name: `omp.${request.method}`,
4079
- kind: "question",
4080
- title: header,
4081
- ...(request.method === "confirm"
4082
- ? { description: this.dataFilter.text(request.message, 64 * 1024) }
4083
- : {}),
4084
- input: {
4085
- questions: [
4086
- {
4087
- header,
4088
- question: this.dataFilter.text(
4089
- request.method === "confirm" ? request.message : request.title,
4090
- 64 * 1024,
4091
- ),
4092
- options: questionOptions,
4093
- multiSelect: false,
4094
- ...(freeformSentinel ? { allowOther: true } : {}),
4095
- ...(request.method === "input" && request.placeholder
4096
- ? { placeholder: this.dataFilter.text(request.placeholder, 4_096) }
4097
- : {}),
4098
- ...((request.method === "input" || request.method === "editor") && request.prefill
4099
- ? { prefill: this.dataFilter.text(request.prefill) }
4100
- : {}),
4101
- },
4102
- ],
4103
- },
4104
- actions,
4105
- },
4106
- });
4107
- }
4108
-
4109
- private rejectPermissionRequest(request: OmpQuestionRequest, description: string): void {
4110
- this.emit({
4111
- type: "session.notice",
4112
- sessionId: this.id,
4113
- notice: {
4114
- id: `omp:permission-rejected:${this.permissionSequence + 1}`,
4115
- severity: "warning",
4116
- title: "OMP question canceled",
4117
- description,
4118
- },
4119
- });
4120
- void this.runtime
4121
- .respondToExtensionUi({ type: "extension_ui_response", id: request.id, cancelled: true })
4122
- .catch(() => this.handleRuntimeFailure());
4123
- }
4124
-
4125
- private freeformSelection(
4126
- pending: PendingPermission,
4127
- response: ProviderPermissionResponse,
4128
- ): string | undefined {
4129
- if (
4130
- pending.request.method !== "select" ||
4131
- !pending.freeformSentinel ||
4132
- response.behavior !== "allow" ||
4133
- response.selectedActionId !== undefined
4134
- ) {
4135
- return;
4136
- }
4137
- const answers = response.updatedInput?.answers;
4138
- const answer =
4139
- answers && typeof answers === "object" && !Array.isArray(answers)
4140
- ? answers[pending.header]
4141
- : undefined;
4142
- const value = Array.isArray(answer) ? answer[0] : answer;
4143
- if (
4144
- typeof value !== "string" ||
4145
- pending.optionValues.has(value) ||
4146
- pending.displayValues.has(value)
4147
- ) {
4148
- return;
4149
- }
4150
- if (
4151
- value === pending.freeformSentinel ||
4152
- value.trim().length === 0 ||
4153
- value.includes("\0") ||
4154
- utf8Bytes(value) > MAX_FREEFORM_RESPONSE_BYTES
4155
- ) {
4156
- throw new OmpPublicError("OMP freeform response is invalid");
4157
- }
4158
- return value;
4159
- }
4160
-
4161
- private extensionUiResponse(
4162
- pending: PendingPermission,
4163
- response: ProviderPermissionResponse,
4164
- ): OmpExtensionUiResponse {
4165
- if (response.selectedActionId !== undefined) {
4166
- const expectedBehavior = pending.actionBehaviors.get(response.selectedActionId);
4167
- if (expectedBehavior === undefined || expectedBehavior !== response.behavior) {
4168
- throw new OmpPublicError("OMP permission action is invalid");
4169
- }
4170
- }
4171
- const { nativeId, request, header } = pending;
4172
- if (request.method === "confirm") {
4173
- if (response.behavior === "deny") {
4174
- return { type: "extension_ui_response", id: nativeId, confirmed: false };
4175
- }
4176
- const answers = response.updatedInput?.answers;
4177
- const answer =
4178
- answers && typeof answers === "object" && !Array.isArray(answers)
4179
- ? answers[header]
4180
- : undefined;
4181
- return {
4182
- type: "extension_ui_response",
4183
- id: nativeId,
4184
- confirmed: typeof answer === "string" ? /^yes$/iu.test(answer.trim()) : true,
4185
- };
4186
- }
4187
- this.reevaluateDeferredPermissionTerminal();
4188
- if (response.behavior === "deny") {
4189
- return { type: "extension_ui_response", id: nativeId, cancelled: true };
4190
- }
4191
- const selectedValue = response.selectedActionId
4192
- ? pending.optionValues.get(response.selectedActionId)
4193
- : undefined;
4194
- if (selectedValue !== undefined) {
4195
- return { type: "extension_ui_response", id: nativeId, value: selectedValue };
4196
- }
4197
- const answers = response.updatedInput?.answers;
4198
- if (!answers || typeof answers !== "object" || Array.isArray(answers)) {
4199
- throw new OmpPublicError("OMP question response requires answers");
4200
- }
4201
- const answer = answers[header];
4202
- const publicValue = Array.isArray(answer) ? answer[0] : answer;
4203
- if (typeof publicValue !== "string") {
4204
- throw new OmpPublicError("OMP question response is invalid");
4205
- }
4206
- if (
4207
- request.method === "select" &&
4208
- pending.freeformSentinel &&
4209
- publicValue !== pending.freeformSentinel &&
4210
- !pending.displayValues.has(publicValue) &&
4211
- !pending.optionValues.has(publicValue)
4212
- ) {
4213
- if (
4214
- publicValue.trim().length === 0 ||
4215
- publicValue.includes("\0") ||
4216
- utf8Bytes(publicValue) > MAX_FREEFORM_RESPONSE_BYTES
4217
- ) {
4218
- throw new OmpPublicError("OMP freeform response is invalid");
4219
- }
4220
- this.pendingFreeformSelection = {
4221
- value: publicValue,
4222
- nativeSelectId: pending.nativeId,
4223
- generation: pending.generation,
4224
- runtime: pending.runtime,
4225
- ...(pending.turnId ? { turnId: pending.turnId } : {}),
4226
- };
4227
- return {
4228
- type: "extension_ui_response",
4229
- id: nativeId,
4230
- value: pending.freeformSentinel,
4231
- };
4232
- }
4233
- if (request.method === "select") {
4234
- const mapped =
4235
- pending.optionValues.get(publicValue) ?? pending.displayValues.get(publicValue);
4236
- if (mapped === undefined) {
4237
- throw new OmpPublicError("OMP selection response is invalid");
4238
- }
4239
- return { type: "extension_ui_response", id: nativeId, value: mapped };
4240
- }
4241
- return { type: "extension_ui_response", id: nativeId, value: publicValue };
4242
- }
4243
-
4244
- private resolvePermissionByNativeId(nativeId: string): void {
4245
- for (const permissions of [this.pendingPermissions, this.inFlightPermissions]) {
4246
- for (const [permissionId, pending] of permissions) {
4247
- if (pending.nativeId !== nativeId) continue;
4248
- permissions.delete(permissionId);
4249
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
4250
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
4251
- this.reevaluateDeferredPermissionTerminal();
4252
- return;
4253
- }
4254
- }
4255
- }
4256
-
4257
- private resolveTurnPermissions(turnId: string): void {
4258
- if (this.pendingFreeformSelection?.turnId === turnId) this.pendingFreeformSelection = null;
4259
- this.resolvePermissions((pending) => pending.turnId === turnId, true);
4260
- this.resolveToolPermissions((pending) => pending.turnId === turnId, true);
4261
- }
4262
- private resolveAllPermissions(cancelNative = false): void {
4263
- this.pendingFreeformSelection = null;
4264
- this.resolvePermissions(() => true, cancelNative);
4265
- this.resolveToolPermissions(() => true, cancelNative);
4266
- }
4267
-
4268
- private resolvePermissions(
4269
- matches: (pending: PendingPermission) => boolean,
4270
- cancelNative: boolean,
4271
- ): void {
4272
- const permissionIds = new Set<string>();
4273
- for (const permissions of [this.pendingPermissions, this.inFlightPermissions]) {
4274
- for (const [permissionId, pending] of permissions) {
4275
- if (!matches(pending)) continue;
4276
- permissions.delete(permissionId);
4277
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
4278
- permissionIds.add(permissionId);
4279
- if (cancelNative && permissions === this.pendingPermissions) {
4280
- void pending.runtime
4281
- .respondToExtensionUi({
4282
- type: "extension_ui_response",
4283
- id: pending.nativeId,
4284
- cancelled: true,
4285
- })
4286
- .catch(() => {
4287
- if (!this.closed && !this.runtimeDead) {
4288
- this.invalidateRuntime("OMP permission cancellation failed");
4289
- }
4290
- });
4291
- }
4292
- }
4293
- }
4294
- for (const permissionId of permissionIds) {
4295
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
4296
- }
4297
- this.reevaluateDeferredPermissionTerminal();
4298
- }
4299
- private resolveToolPermissions(
4300
- matches: (pending: PendingToolPermission) => boolean,
4301
- cancelNative: boolean,
4302
- ): void {
4303
- const permissionIds = new Set<string>();
4304
- for (const permissions of [this.pendingToolPermissions, this.inFlightToolPermissions]) {
4305
- for (const [permissionId, pending] of permissions) {
4306
- if (!matches(pending)) continue;
4307
- permissions.delete(permissionId);
4308
- if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
4309
- this.resolvedToolApprovalIds.add(pending.nativeId);
4310
- permissionIds.add(permissionId);
4311
- if (cancelNative && permissions === this.pendingToolPermissions) {
4312
- void pending.runtime
4313
- .respondToToolApproval({
4314
- type: "tool_approval_response",
4315
- id: pending.nativeId,
4316
- toolCallId: pending.toolCallId,
4317
- cancelled: true,
4318
- })
4319
- .catch(() => {
4320
- if (!this.closed && !this.runtimeDead) {
4321
- this.invalidateRuntime("OMP tool permission cancellation failed");
4322
- }
4323
- });
4324
- }
4325
- }
4326
- }
4327
- for (const permissionId of permissionIds) {
4328
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
4329
- }
4330
- this.reevaluateDeferredPermissionTerminal();
4331
- }
4332
-
4333
- private armToolPermissionTimeout(permissionId: string, pending: PendingToolPermission): void {
4334
- if (pending.expiresAt === undefined) return;
4335
- const remainingMs = Math.max(0, pending.expiresAt - Date.now());
4336
- pending.timer = this.scheduler.set(() => {
4337
- if (this.pendingToolPermissions.get(permissionId) !== pending) return;
4338
- this.pendingToolPermissions.delete(permissionId);
4339
- this.inFlightToolPermissions.set(permissionId, pending);
4340
- void pending.runtime
4341
- .respondToToolApproval({
4342
- type: "tool_approval_response",
4343
- id: pending.nativeId,
4344
- toolCallId: pending.toolCallId,
4345
- cancelled: true,
4346
- timedOut: true,
4347
- })
4348
- .then(
4349
- () => {
4350
- if (this.inFlightToolPermissions.get(permissionId) !== pending) return;
4351
- this.inFlightToolPermissions.delete(permissionId);
4352
- this.resolvedToolApprovalIds.add(pending.nativeId);
4353
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
4354
- this.reevaluateDeferredPermissionTerminal();
4355
- },
4356
- () => this.handleRuntimeFailure(),
4357
- );
4358
- }, remainingMs);
4359
- }
4360
-
4361
- private armPermissionTimeout(permissionId: string, pending: PendingPermission): void {
4362
- if (pending.expiresAt === undefined) return;
4363
- const remainingMs = Math.max(0, pending.expiresAt - Date.now());
4364
- pending.timer = this.scheduler.set(() => {
4365
- if (this.pendingPermissions.get(permissionId) !== pending) return;
4366
- this.pendingPermissions.delete(permissionId);
4367
- if (!this.permissionOwnerIsCurrent(pending)) {
4368
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
4369
- this.reevaluateDeferredPermissionTerminal();
4370
- return;
4371
- }
4372
- this.inFlightPermissions.set(permissionId, pending);
4373
- void pending.runtime
4374
- .respondToExtensionUi({
4375
- type: "extension_ui_response",
4376
- id: pending.nativeId,
4377
- cancelled: true,
4378
- timedOut: true,
4379
- })
4380
- .then(
4381
- () => {
4382
- if (this.inFlightPermissions.get(permissionId) !== pending) return;
4383
- this.inFlightPermissions.delete(permissionId);
4384
- this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
4385
- this.reevaluateDeferredPermissionTerminal();
4386
- },
4387
- () => this.handleRuntimeFailure(),
4388
- );
4389
- }, remainingMs);
4390
- }
4391
-
4392
- private permissionOwnerIsCurrent(pending: PendingPermission | PendingToolPermission): boolean {
4393
- if (
4394
- this.closed ||
4395
- this.runtimeDead ||
4396
- pending.generation !== this.generation ||
4397
- pending.runtime !== this.runtime
4398
- ) {
4399
- return false;
4400
- }
4401
- if (pending.turnId === undefined) return true;
4402
- return this.activeTurn?.turnId === pending.turnId && !this.activeTurn.terminal;
4403
- }
4404
2708
 
4405
2709
  private hasTerminalConflict(turn: ActiveTurn, includeChildren = true): boolean {
4406
- const ownsTurn = (pending: PendingPermission | PendingToolPermission) =>
4407
- pending.turnId === turn.turnId;
4408
2710
  return (
4409
2711
  turn.awaitingPermissionEvidence ||
4410
- [...this.pendingPermissions.values()].some(ownsTurn) ||
4411
- [...this.inFlightPermissions.values()].some(ownsTurn) ||
4412
- [...this.pendingToolPermissions.values()].some(ownsTurn) ||
4413
- [...this.inFlightToolPermissions.values()].some(ownsTurn) ||
2712
+ this.permissions.hasForTurn(turn.turnId) ||
4414
2713
  turn.activeToolCallIds.size > 0 ||
4415
2714
  turn.steersInFlight > 0 ||
4416
2715
  (includeChildren && Boolean(this.subsessions?.hasActiveChildren()))
@@ -4546,7 +2845,7 @@ export class OmpProviderSession {
4546
2845
  await Promise.allSettled(turn.userLookups);
4547
2846
  this.refreshAmbiguousTerminal(turn);
4548
2847
  if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
4549
- const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
2848
+ const state = await this.usage.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4550
2849
  if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
4551
2850
  if (!state) {
4552
2851
  turn.operationalTerminalStage = "unresolved";
@@ -4576,10 +2875,7 @@ export class OmpProviderSession {
4576
2875
  ) {
4577
2876
  return;
4578
2877
  }
4579
- turn.usageSampleFloor = this.usageSequence + 1;
4580
- if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
4581
- this.usageSample = null;
4582
- }
2878
+ this.usage.invalidateBeforeNextSample(turn);
4583
2879
  this.publishPendingUsers(turn);
4584
2880
  await this.finishTurn(turn, "completed", undefined, false, false, true);
4585
2881
  }
@@ -4608,7 +2904,7 @@ export class OmpProviderSession {
4608
2904
  this.beginTerminalization(turn, replacement);
4609
2905
  return;
4610
2906
  }
4611
- this.pollUsage(turn);
2907
+ this.usage.poll(turn);
4612
2908
  }
4613
2909
 
4614
2910
  private beginTerminalization(turn: ActiveTurn, candidate: TerminalCandidate): void {
@@ -4622,11 +2918,8 @@ export class OmpProviderSession {
4622
2918
  }
4623
2919
  turn.terminalizing = true;
4624
2920
  this.cancelAmbiguousTerminal(turn);
4625
- turn.usageSampleFloor = this.usageSequence + 1;
4626
- if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
4627
- this.usageSample = null;
4628
- }
4629
- this.stopUsagePoll(turn);
2921
+ this.usage.invalidateBeforeNextSample(turn);
2922
+ this.usage.stopPoll(turn);
4630
2923
  turn.agentEndDeadlineTimer = this.scheduler.set(() => {
4631
2924
  turn.agentEndDeadlineTimer = undefined;
4632
2925
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
@@ -4718,14 +3011,14 @@ export class OmpProviderSession {
4718
3011
  ) {
4719
3012
  this.resetAgentEndProbe(turn);
4720
3013
  turn.deferredAgentEnd = candidate;
4721
- this.pollUsage(turn);
3014
+ this.usage.poll(turn);
4722
3015
  return;
4723
3016
  }
4724
3017
  if (turn.interrupted) {
4725
3018
  await this.completeAgentEnd(turn, candidate.event);
4726
3019
  return;
4727
3020
  }
4728
- const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
3021
+ const state = await this.usage.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4729
3022
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
4730
3023
  if (!turn.interrupted && this.subsessions?.hasActiveChildren()) {
4731
3024
  this.resetAgentEndProbe(turn);
@@ -4738,7 +3031,7 @@ export class OmpProviderSession {
4738
3031
  ) {
4739
3032
  this.resetAgentEndProbe(turn);
4740
3033
  turn.deferredAgentEnd = candidate;
4741
- this.pollUsage(turn);
3034
+ this.usage.poll(turn);
4742
3035
  return;
4743
3036
  }
4744
3037
  if (state) {
@@ -4780,7 +3073,7 @@ export class OmpProviderSession {
4780
3073
  !turn.interrupted &&
4781
3074
  this.activeTurn === turn
4782
3075
  ) {
4783
- const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
3076
+ const state = await this.usage.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4784
3077
  if (
4785
3078
  turn.terminal ||
4786
3079
  this.activeTurn !== turn ||
@@ -4858,9 +3151,9 @@ export class OmpProviderSession {
4858
3151
  turn.steerReady.resolve();
4859
3152
  turn.starting = false;
4860
3153
  turn.terminal = true;
4861
- this.stopUsagePoll(turn);
3154
+ this.usage.stopPoll(turn);
4862
3155
  this.cancelAmbiguousTerminal(turn);
4863
- this.resolveTurnPermissions(turn.turnId);
3156
+ this.permissions.resolveTurnPermissions(turn.turnId);
4864
3157
  this.projector.finishTurn(turn.turnId);
4865
3158
  if (this.activeTurn === turn) this.activeTurn = null;
4866
3159
  }
@@ -4885,7 +3178,7 @@ export class OmpProviderSession {
4885
3178
  turn.starting = false;
4886
3179
  turn.steerReady.resolve();
4887
3180
  this.emit({ type: "session.turn", sessionId: this.id, turnId: turn.turnId, state: "started" });
4888
- if (pollUsage) this.pollUsage(turn);
3181
+ if (pollUsage) this.usage.poll(turn);
4889
3182
  }
4890
3183
  private finishTurn(
4891
3184
  turn: ActiveTurn,
@@ -4915,7 +3208,7 @@ export class OmpProviderSession {
4915
3208
  turn.agentEndPending = false;
4916
3209
  this.cancelLocalOnlyCompletion(turn);
4917
3210
  this.cancelAmbiguousTerminal(turn);
4918
- this.stopUsagePoll(turn);
3211
+ this.usage.stopPoll(turn);
4919
3212
  if (turn.manualCompactionDeadlineTimer !== undefined) {
4920
3213
  this.scheduler.clear(turn.manualCompactionDeadlineTimer);
4921
3214
  turn.manualCompactionDeadlineTimer = undefined;
@@ -4934,7 +3227,7 @@ export class OmpProviderSession {
4934
3227
  const terminalization = (async () => {
4935
3228
  if (!turn.terminalOutcome?.usageSampled && !this.closed && !this.runtimeDead) {
4936
3229
  await Promise.race([
4937
- this.boundedUsageSnapshot(turn, FINAL_USAGE_WAIT_MS),
3230
+ this.usage.boundedSnapshot(turn, FINAL_USAGE_WAIT_MS),
4938
3231
  wake.promise.then(() => undefined),
4939
3232
  ]);
4940
3233
  }
@@ -4961,17 +3254,17 @@ export class OmpProviderSession {
4961
3254
  }
4962
3255
  this.imageMaterializer.clear();
4963
3256
  turn.terminal = true;
4964
- if (this.usageSample?.turn === turn) this.usageSample = null;
4965
- if (!preserveCompactions && this.activeCompaction) {
4966
- if (outcome.state === "canceled") this.finishCompaction("canceled");
3257
+ this.usage.clearSample(turn);
3258
+ if (!preserveCompactions && this.compaction.active) {
3259
+ if (outcome.state === "canceled") this.compaction.finish("canceled");
4967
3260
  else {
4968
- this.finishCompaction("failed", {
3261
+ this.compaction.finish("failed", {
4969
3262
  message: outcome.error?.message ?? "OMP compaction ended without a terminal result",
4970
3263
  });
4971
3264
  }
4972
3265
  }
4973
3266
  this.publishPendingUsers(turn);
4974
- this.resolveTurnPermissions(turn.turnId);
3267
+ this.permissions.resolveTurnPermissions(turn.turnId);
4975
3268
  try {
4976
3269
  this.projector.finishTurn(turn.turnId, preserveCompactions);
4977
3270
  } catch (error) {
@@ -5002,15 +3295,14 @@ export class OmpProviderSession {
5002
3295
  ): void {
5003
3296
  this.imageMaterializer.clear();
5004
3297
  if (this.closed || this.runtimeDead) return;
5005
- this.discardedCompactionEnds = 0;
5006
- this.resolveAllPermissions();
3298
+ this.compaction.resetDiscardedEnds();
3299
+ this.permissions.resolveAllPermissions();
5007
3300
  this.recoveryUsesNativeConfig ||=
5008
3301
  this.configRefreshInFlight !== null || this.configRefreshDirty || this.configMutationInFlight;
5009
3302
  const turn = this.activeTurn;
5010
- if (turn) this.stopUsagePoll(turn);
5011
- this.usageSample = null;
5012
- this.finishCompaction(compactionState, { message });
5013
- this.lastUsage = null;
3303
+ if (turn) this.usage.stopPoll(turn);
3304
+ this.usage.reset();
3305
+ this.compaction.finish(compactionState, { message });
5014
3306
  this.generation += 1;
5015
3307
  this.projector.resetRuntimeGeneration("OMP runtime ended during compaction");
5016
3308
  this.runtimeDead = message;