@omercnet/paseo-omp 0.2.1-next.72.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 (78) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -0
  4. package/SUPPORT.md +42 -0
  5. package/TESTING.md +150 -0
  6. package/client/composer-pill-settings.tsx +157 -0
  7. package/client/hub-icon.tsx +12 -0
  8. package/client/hub-popover.tsx +132 -0
  9. package/client/hub-status.ts +29 -0
  10. package/client/mcp-authorization.tsx +168 -0
  11. package/client/mcp-popover.tsx +155 -0
  12. package/client/memory-panel.tsx +76 -0
  13. package/client/memory-popover.tsx +74 -0
  14. package/client/omp-config-surface.tsx +1433 -0
  15. package/client/omp-doc-links.ts +117 -0
  16. package/client/omp-plugin-manager.tsx +1004 -0
  17. package/client/omp-store-picker.tsx +89 -0
  18. package/client/omp-store-state.ts +45 -0
  19. package/client/provider-diagnostics-state.ts +262 -0
  20. package/client/provider-icon.tsx +27 -0
  21. package/client/provider-image.tsx +66 -0
  22. package/client/quota-popover.tsx +155 -0
  23. package/client/quota-state.ts +140 -0
  24. package/client/sessions-popover.tsx +78 -0
  25. package/docs/alpha-release-checklist.md +68 -0
  26. package/docs/configuration.md +126 -0
  27. package/docs/core-provider-issue-audit.md +108 -0
  28. package/docs/images/mcp-authorization-compact.png +0 -0
  29. package/docs/images/mcp-controls-wide.png +0 -0
  30. package/docs/images/plugin-manager.png +0 -0
  31. package/docs/images/workspace-settings.png +0 -0
  32. package/docs/installation.md +67 -0
  33. package/index.client.tsx +488 -0
  34. package/index.server.ts +81 -0
  35. package/package.json +84 -0
  36. package/paseo-plugin.json +5 -0
  37. package/scripts/prepare-dependencies.mjs +20 -0
  38. package/server/hub.ts +145 -0
  39. package/server/mcp-browser.ts +95 -0
  40. package/server/memory.ts +86 -0
  41. package/server/mutation-queue.ts +12 -0
  42. package/server/omp-config.ts +135 -0
  43. package/server/omp-plugins.ts +676 -0
  44. package/server/omp-settings.ts +499 -0
  45. package/server/paths.ts +181 -0
  46. package/server/provider/catalog.ts +172 -0
  47. package/server/provider/config-normalization.ts +148 -0
  48. package/server/provider/connection.ts +1196 -0
  49. package/server/provider/host-tools.ts +777 -0
  50. package/server/provider/image.ts +143 -0
  51. package/server/provider/mcp-transport.ts +394 -0
  52. package/server/provider/omp-rpc.ts +2806 -0
  53. package/server/provider/omp.svg +5 -0
  54. package/server/provider/profile-providers.ts +249 -0
  55. package/server/provider/provider-options.ts +27 -0
  56. package/server/provider/registration.ts +162 -0
  57. package/server/provider/security.ts +317 -0
  58. package/server/provider/session-descriptors.ts +736 -0
  59. package/server/provider/session.ts +4796 -0
  60. package/server/provider/settings.ts +78 -0
  61. package/server/provider/subsessions.ts +850 -0
  62. package/server/provider/timeline-projector.ts +1801 -0
  63. package/server/provider-diagnostics.ts +1143 -0
  64. package/server/quota.ts +55 -0
  65. package/server/sessions.ts +58 -0
  66. package/shared/composer-pill-settings.ts +28 -0
  67. package/shared/hub.ts +43 -0
  68. package/shared/mcp.ts +47 -0
  69. package/shared/memory.ts +24 -0
  70. package/shared/omp-config.ts +85 -0
  71. package/shared/omp-plugins.ts +264 -0
  72. package/shared/omp-settings.ts +214 -0
  73. package/shared/omp-store.ts +58 -0
  74. package/shared/provider-diagnostics.ts +126 -0
  75. package/shared/provider-image.ts +160 -0
  76. package/shared/quota.ts +23 -0
  77. package/shared/sessions.ts +24 -0
  78. package/tsconfig.json +16 -0
@@ -0,0 +1,4796 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import type {
3
+ ProviderConfigState,
4
+ ProviderContent,
5
+ ProviderError,
6
+ ProviderEvent,
7
+ ProviderInput,
8
+ ProviderPermissionResponse,
9
+ ProviderSessionConfig,
10
+ ProviderToolCallDetail,
11
+ ProviderUsage,
12
+ } from "@getpaseo/plugin/server/provider";
13
+ import { getForgeDefinitionOrNeutral } from "@getpaseo/protocol/forge-manifest";
14
+ import { mapOmpModels, nativeOmpModelId, OMP_MODES, ompModelId, thinkingForModel } from "./catalog";
15
+ import {
16
+ normalizeOmpSessionConfig,
17
+ type OmpRecoveryOptions,
18
+ withCommittedOmpSelection,
19
+ } from "./config-normalization";
20
+ import { OmpHostToolsBridge, type OmpMcpConnector, validateOmpHostToolConfig } from "./host-tools";
21
+ import { isOmpImageMimeType, isValidImagePayload, OmpImageMaterializer } from "./image";
22
+ import type {
23
+ OmpAvailableCommand,
24
+ OmpCompactionResult,
25
+ OmpExtensionUiResponse,
26
+ OmpImage,
27
+ OmpMessage,
28
+ OmpModel,
29
+ OmpRpcEvent,
30
+ OmpRuntime,
31
+ OmpRuntimeSession,
32
+ OmpSessionState,
33
+ OmpSessionStats,
34
+ OmpStartOptions,
35
+ OmpToolApprovalCancel,
36
+ OmpToolApprovalRequest,
37
+ } from "./omp-rpc";
38
+ import { buildOmpSpawnRequest } from "./omp-rpc";
39
+ import {
40
+ BoundedStringSet,
41
+ boundedJsonBytes,
42
+ configuredOutputRedactionValues,
43
+ isOmpCleanupFailure,
44
+ isOmpPublicError,
45
+ OmpCleanupFailure,
46
+ OmpPublicDataSerializer,
47
+ OmpPublicError,
48
+ utf8Bytes,
49
+ } from "./security";
50
+ import type { OmpSessionDescriptor } from "./session-descriptors";
51
+ import { validateNativeSessionId } from "./session-descriptors";
52
+ import { OmpSubsessionProjector } from "./subsessions";
53
+ import {
54
+ defaultOmpTimelineScheduler,
55
+ OmpTimelineProjector,
56
+ type OmpTimelineScheduler,
57
+ } from "./timeline-projector";
58
+
59
+ type SessionOpenInput = Extract<ProviderInput, { type: "session.open" }>;
60
+ type SessionPromptInput = Extract<ProviderInput, { type: "session.prompt" }>;
61
+ type SessionInterruptInput = Extract<ProviderInput, { type: "session.interrupt" }>;
62
+ type SessionConfigureInput = Extract<ProviderInput, { type: "session.configure" }>;
63
+ type SessionRevertInput = Extract<ProviderInput, { type: "session.revert" }>;
64
+ type SessionCloseInput = Extract<ProviderInput, { type: "session.close" }>;
65
+ type NativeSessionTransition = (previousSessionId: string, nextSessionId: string) => void;
66
+ type RewindCleanupQuarantine = (cleanup: Promise<void>) => void;
67
+ type RewindSessionRetirement = () => void;
68
+ type SessionPermissionInput = Extract<ProviderInput, { type: "session.permission" }>;
69
+ type OmpQuestionRequest = Extract<
70
+ Extract<OmpRpcEvent, { type: "extension_ui_request" }>,
71
+ { method: "select" | "confirm" | "input" | "editor" }
72
+ >;
73
+ type Emit = (event: ProviderEvent) => void;
74
+ const LOCAL_ONLY_SETTLE_MS = 5_000;
75
+ const AGENT_END_STATE_TIMEOUT_MS = 2_000;
76
+ const AGENT_END_HISTORY_TIMEOUT_MS = 2_000;
77
+ const CONFIG_REFRESH_RETRY_BASE_MS = 250;
78
+ const CONFIG_REFRESH_MAX_ATTEMPTS = 3;
79
+ const USAGE_POLL_MS = 1_000;
80
+ const USAGE_REFRESH_MS = 100;
81
+ const FINAL_USAGE_WAIT_MS = 250;
82
+ const COMPACTION_MAX_WAIT_MS = 5 * 60_000;
83
+ const AGENT_END_SETTLE_MS = 5_000;
84
+ const MAX_PROMPT_PARTS = 64;
85
+ const MAX_PROMPT_TEXT_LENGTH = 1024 * 1024;
86
+ const RPC_REQUEST_ID_BYTES = 36;
87
+ const MAX_AGENT_END_CORRELATION_MESSAGES = 512;
88
+ const MAX_TRACKED_ENTRY_IDS = 1_024;
89
+ const MAX_UNCLAIMED_BRANCH_ENTRIES = 1_024;
90
+ const MAX_PENDING_USERS = 256;
91
+ const MAX_PENDING_PERMISSIONS = 32;
92
+ const MAX_PENDING_PERMISSION_BYTES = 2 * 1024 * 1024;
93
+ const MAX_USER_ECHOES = 512;
94
+ const MAX_BUFFERED_TURN_EVENTS = 512;
95
+ const MAX_BUFFERED_VALUE_ITEMS = 1_024;
96
+ const MAX_BUFFERED_VALUE_NODES = 4_096;
97
+ const MAX_BUFFERED_TURN_BYTES = 4 * 1024 * 1024;
98
+ const MAX_USER_ECHO_BYTES = 2 * 1024 * 1024;
99
+ const MAX_PENDING_USER_BYTES = 2 * 1024 * 1024;
100
+ const MAX_UNCLAIMED_BRANCH_BYTES = 4 * 1024 * 1024;
101
+ class OmpCatalogEscape extends OmpPublicError {}
102
+ const MAX_REPLAY_MESSAGES = 100_000;
103
+ const REPLAY_TIMEOUT_MS = 20_000;
104
+ const OMP_ASK_USER_FREEFORM_SENTINEL = "✏️ Type custom response...";
105
+ const MAX_FREEFORM_RESPONSE_BYTES = 64 * 1024;
106
+ const OMP_BUILTIN_COMMANDS: readonly OmpAvailableCommand[] = [
107
+ {
108
+ name: "compact",
109
+ description: "Manually compact the session context",
110
+ input: { hint: "[instructions]" },
111
+ source: "builtin",
112
+ },
113
+ {
114
+ name: "autocompact",
115
+ description: "Toggle automatic context compaction",
116
+ input: { hint: "[on|off|toggle]" },
117
+ source: "builtin",
118
+ },
119
+ {
120
+ name: "handoff",
121
+ description: "Hand off from planning to implementation",
122
+ input: { hint: "[instructions]" },
123
+ source: "builtin",
124
+ },
125
+ {
126
+ name: "steer",
127
+ description: "Steer the active OMP turn",
128
+ input: { hint: "<message>" },
129
+ source: "builtin",
130
+ },
131
+ {
132
+ name: "follow-up",
133
+ description: "Queue a follow-up message for OMP",
134
+ input: { hint: "<message>" },
135
+ source: "builtin",
136
+ },
137
+ ];
138
+
139
+ function applicableThinkingLevel(
140
+ model: OmpModel | undefined,
141
+ level: OmpSessionState["thinkingLevel"],
142
+ ): OmpSessionState["thinkingLevel"] {
143
+ return model?.reasoning === false ? undefined : level;
144
+ }
145
+
146
+ function fixedSessionMode(modeId = "full") {
147
+ const mode = OMP_MODES.find((candidate) => candidate.id === modeId);
148
+ if (!mode) throw new OmpPublicError("OMP mode is unavailable");
149
+ return {
150
+ ...mode,
151
+ label: `${mode.label} (fixed for session)`,
152
+ description: `${mode.description} Approval mode is fixed for this session; create a new session to choose another mode.`,
153
+ };
154
+ }
155
+
156
+ export function ompPersistenceSessionId(input: SessionOpenInput): string | undefined {
157
+ if (!input.persistence) return;
158
+ if (input.persistence.version !== 1) {
159
+ throw new OmpPublicError("Unsupported OMP persistence version");
160
+ }
161
+ const data = input.persistence.data;
162
+ if (
163
+ !data ||
164
+ typeof data !== "object" ||
165
+ Array.isArray(data) ||
166
+ !Object.hasOwn(data, "sessionId") ||
167
+ Object.keys(data).length !== 1
168
+ ) {
169
+ throw new OmpPublicError("Invalid OMP session persistence");
170
+ }
171
+ try {
172
+ return validateNativeSessionId((data as Record<string, unknown>).sessionId);
173
+ } catch {
174
+ throw new OmpPublicError("Invalid OMP session identifier");
175
+ }
176
+ }
177
+
178
+ async function authorizeNativeSession(
179
+ runtime: OmpRuntime,
180
+ sessionId: string,
181
+ cwd: string,
182
+ sessionDir?: string,
183
+ ): Promise<OmpSessionDescriptor> {
184
+ const matches = await runtime.listSessions({ sessionId, cwd, limit: 2, sessionDir });
185
+ const descriptor = matches[0];
186
+ if (matches.length !== 1 || descriptor?.id !== sessionId) {
187
+ throw new OmpPublicError("OMP session could not be resolved in this workspace");
188
+ }
189
+ if (descriptor.cwd !== cwd) {
190
+ throw new OmpPublicError("OMP session belongs to a different working directory");
191
+ }
192
+ return descriptor;
193
+ }
194
+
195
+ function retainedBytes(values: readonly unknown[], maxBytes: number): number {
196
+ let total = 0;
197
+ for (const value of values) {
198
+ const bytes = boundedJsonBytes(
199
+ value,
200
+ maxBytes,
201
+ MAX_BUFFERED_VALUE_ITEMS,
202
+ maxBytes,
203
+ MAX_BUFFERED_VALUE_NODES,
204
+ );
205
+ if (bytes === Number.POSITIVE_INFINITY) return bytes;
206
+ total += bytes;
207
+ if (total > maxBytes) return Number.POSITIVE_INFINITY;
208
+ }
209
+ return total;
210
+ }
211
+
212
+ async function waitForReplay<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
213
+ signal.throwIfAborted();
214
+ const aborted = Promise.withResolvers<never>();
215
+ const onAbort = () => aborted.reject(signal.reason);
216
+ signal.addEventListener("abort", onAbort, { once: true });
217
+ try {
218
+ return await Promise.race([operation, aborted.promise]);
219
+ } finally {
220
+ signal.removeEventListener("abort", onAbort);
221
+ }
222
+ }
223
+
224
+ type PendingUser = {
225
+ clientMessageId: string;
226
+ text: string;
227
+ accepted: boolean;
228
+ fallbackOnFinish: boolean;
229
+ bufferedEchoes: OmpMessage[];
230
+ };
231
+
232
+ type ActiveTurn = {
233
+ turnId: string;
234
+ clientMessageId: string;
235
+ generation: number;
236
+ promptResultEmitted: boolean;
237
+ started: boolean;
238
+ terminal: boolean;
239
+ interrupted: boolean;
240
+ starting: boolean;
241
+ nativeActivity: boolean;
242
+ userEchoObserved: boolean;
243
+ localOnlyDisabled: boolean;
244
+ localOnlyEligible: boolean;
245
+ awaitingPermissionEvidence: boolean;
246
+ activitySequence: number;
247
+ acknowledged: boolean;
248
+ terminalOwnershipEvidence: boolean;
249
+ terminalOwnershipRequired: boolean;
250
+ replayingBufferedEvents: boolean;
251
+ bufferedTerminalOwnershipEvidence: boolean;
252
+ agentInvoked?: boolean;
253
+ nativeRequestId?: string;
254
+ promptAcceptedEventIndex?: number;
255
+ localOnlyTimer?: unknown;
256
+ usagePollTimer?: unknown;
257
+ usagePoll?: Promise<void>;
258
+ usageSampleFloor: number;
259
+ manualCompaction: boolean;
260
+ manualCompactionPending: boolean;
261
+ manualCompactionDeadlineTimer?: unknown;
262
+ agentEndPending: boolean;
263
+ agentEndRetryTimer?: unknown;
264
+ agentEndDeadlineTimer?: unknown;
265
+ agentEndCheck?: Promise<void>;
266
+ terminalOwnershipTimer?: unknown;
267
+ terminalizing: boolean;
268
+ terminalization?: Promise<void>;
269
+ terminalOutcome?: TurnOutcome;
270
+ terminalWake?: VoidDeferred;
271
+ steerReady: VoidDeferred;
272
+ steersInFlight: number;
273
+ deferredAgentEnd?: Extract<OmpRpcEvent, { type: "agent_end" }>;
274
+ bufferedEvents: OmpRpcEvent[];
275
+ pendingUsers: PendingUser[];
276
+ userEchoes: OmpMessage[];
277
+ userCorrelationActive: boolean;
278
+ userLookups: Set<Promise<void>>;
279
+ completedMessageCount: number;
280
+ streamedMessageEntryIds: string[];
281
+ streamedMessageIdentityComplete: boolean;
282
+ lastCompletedAssistantOutcome?: AgentEndOutcome;
283
+ lastCompletedAssistantEntryId?: string;
284
+ };
285
+
286
+ type PendingAbort = {
287
+ turn: ActiveTurn;
288
+ generation: number;
289
+ runtime: OmpRuntimeSession;
290
+ forceTerminal: boolean;
291
+ promise: Promise<void>;
292
+ };
293
+ type PendingPermission = {
294
+ nativeId: string;
295
+ header: string;
296
+ fingerprint: string;
297
+ optionValues: ReadonlyMap<string, string>;
298
+ actionBehaviors: ReadonlyMap<string, "allow" | "deny">;
299
+ retainedBytes: number;
300
+ displayValues: ReadonlyMap<string, string>;
301
+ generation: number;
302
+ runtime: OmpRuntimeSession;
303
+ expiresAt?: number;
304
+ timer?: unknown;
305
+ turnId?: string;
306
+ request: OmpQuestionRequest;
307
+ freeformSentinel?: string;
308
+ };
309
+ type PendingToolPermission = {
310
+ nativeId: string;
311
+ toolCallId: string;
312
+ fingerprint: string;
313
+ retainedBytes: number;
314
+ generation: number;
315
+ runtime: OmpRuntimeSession;
316
+ expiresAt?: number;
317
+ timer?: unknown;
318
+ turnId?: string;
319
+ };
320
+
321
+ type PendingFreeformSelection = {
322
+ value: string;
323
+ nativeSelectId: string;
324
+ generation: number;
325
+ runtime: OmpRuntimeSession;
326
+ turnId?: string;
327
+ };
328
+
329
+ function permissionFingerprint(request: OmpQuestionRequest): string {
330
+ return createHash("sha256").update(JSON.stringify(request)).digest("base64url");
331
+ }
332
+
333
+ type VoidDeferred = {
334
+ promise: Promise<void>;
335
+ resolve(value?: void | PromiseLike<void>): void;
336
+ reject(reason?: unknown): void;
337
+ };
338
+
339
+ type TurnOutcome = {
340
+ state: "completed" | "failed" | "canceled";
341
+ error?: ProviderError;
342
+ usageSampled: boolean;
343
+ };
344
+
345
+ type ActiveCompaction = {
346
+ id: string;
347
+ trigger: "auto" | "manual";
348
+ turnId: string;
349
+ generation: number;
350
+ retrying: boolean;
351
+ action?: string;
352
+ preTokens?: number;
353
+ };
354
+
355
+ function providerError(error: unknown, fallback: string): { message: string } {
356
+ return { message: isOmpPublicError(error) ? error.message : fallback };
357
+ }
358
+ async function settleSessionCleanup(promises: readonly Promise<void>[]): Promise<void> {
359
+ const pending = [...promises];
360
+ const seen = new Set<Promise<void>>();
361
+ const failures: unknown[] = [];
362
+ while (pending.length > 0) {
363
+ const batch = pending.splice(0).filter((promise) => !seen.has(promise));
364
+ for (const promise of batch) seen.add(promise);
365
+ const results = await Promise.allSettled(batch);
366
+ for (const result of results) {
367
+ if (result.status !== "rejected") continue;
368
+ if (isOmpCleanupFailure(result.reason)) {
369
+ if (!seen.has(result.reason.cleanup)) pending.push(result.reason.cleanup);
370
+ continue;
371
+ }
372
+ failures.push(result.reason);
373
+ }
374
+ }
375
+ if (failures.length > 0) {
376
+ throw new AggregateError(failures, "OMP session initialization cleanup failed");
377
+ }
378
+ }
379
+ function isSafeCommandName(name: string): boolean {
380
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*(?::[A-Za-z0-9][A-Za-z0-9._-]*)*$/u.test(name);
381
+ }
382
+
383
+ type OmpPromptPayload = { text: string; images: OmpImage[]; commandName?: string };
384
+
385
+ const REVIEW_LINE_MARKERS = { add: "+", remove: "-", context: " " } as const;
386
+
387
+ function renderPromptAttachmentAsText(part: Exclude<ProviderContent, { type: "image" }>): string {
388
+ switch (part.type) {
389
+ case "forge_change_request": {
390
+ return renderChangeRequestAttachment({
391
+ forge: part.forge ?? "github",
392
+ number: part.number,
393
+ title: part.title,
394
+ url: part.url,
395
+ body: part.body,
396
+ projectPath: part.projectPath,
397
+ baseRefName: part.baseRefName,
398
+ headRefName: part.headRefName,
399
+ });
400
+ }
401
+ case "github_pr": {
402
+ return renderChangeRequestAttachment({
403
+ forge: "github",
404
+ number: part.number,
405
+ title: part.title,
406
+ url: part.url,
407
+ body: part.body,
408
+ baseRefName: part.baseRefName,
409
+ headRefName: part.headRefName,
410
+ });
411
+ }
412
+ case "forge_issue": {
413
+ return renderIssueAttachment({
414
+ forge: part.forge ?? "github",
415
+ number: part.number,
416
+ title: part.title,
417
+ url: part.url,
418
+ body: part.body,
419
+ projectPath: part.projectPath,
420
+ });
421
+ }
422
+ case "github_issue": {
423
+ return renderIssueAttachment({
424
+ forge: "github",
425
+ number: part.number,
426
+ title: part.title,
427
+ url: part.url,
428
+ body: part.body,
429
+ });
430
+ }
431
+ case "text": {
432
+ return part.text;
433
+ }
434
+ case "review": {
435
+ const lines = [`Paseo review attachment (${part.mode})`, `CWD: ${part.cwd}`];
436
+ if (part.baseRef) {
437
+ lines.push(`Base: ${part.baseRef}`);
438
+ }
439
+ part.comments.forEach((comment, index) => {
440
+ lines.push(
441
+ "",
442
+ `Comment ${index + 1}: ${comment.filePath}:${comment.side}:${comment.lineNumber}`,
443
+ comment.body,
444
+ comment.context.hunkHeader,
445
+ );
446
+ const target = comment.context.targetLine;
447
+ for (const line of comment.context.lines) {
448
+ const isTarget =
449
+ line.oldLineNumber === target.oldLineNumber &&
450
+ line.newLineNumber === target.newLineNumber &&
451
+ line.type === target.type &&
452
+ line.content === target.content;
453
+ const prefix = isTarget ? "> " : " ";
454
+ const oldLn = padLineNumber(line.oldLineNumber);
455
+ const newLn = padLineNumber(line.newLineNumber);
456
+ lines.push(`${prefix}${oldLn} ${newLn} ${REVIEW_LINE_MARKERS[line.type]}${line.content}`);
457
+ }
458
+ });
459
+ return lines.join("\n");
460
+ }
461
+ case "uploaded_file": {
462
+ return [
463
+ `Uploaded file: ${part.fileName}`,
464
+ `Path: ${part.path}`,
465
+ `MIME: ${part.mimeType}`,
466
+ `Size: ${part.size} bytes`,
467
+ ].join("\n");
468
+ }
469
+ default:
470
+ throw new Error("unreachable");
471
+ }
472
+ }
473
+
474
+ function renderChangeRequestAttachment(input: {
475
+ forge: string;
476
+ number: number;
477
+ title: string;
478
+ url: string;
479
+ body?: string | null;
480
+ projectPath?: string;
481
+ baseRefName?: string | null;
482
+ headRefName?: string | null;
483
+ }): string {
484
+ const lines = [
485
+ `${formatForgeLabel(input.forge)} ${formatChangeRequestAbbrev(input.forge)} ${formatChangeRequestNumber(input.forge, input.number)}: ${input.title}`,
486
+ input.url,
487
+ ];
488
+ if (input.projectPath) {
489
+ lines.push(`Project: ${input.projectPath}`);
490
+ }
491
+ if (input.baseRefName) {
492
+ lines.push(`Base: ${input.baseRefName}`);
493
+ }
494
+ if (input.headRefName) {
495
+ lines.push(`Head: ${input.headRefName}`);
496
+ }
497
+ if (input.body) {
498
+ lines.push("", input.body);
499
+ }
500
+ return lines.join("\n");
501
+ }
502
+
503
+ function renderIssueAttachment(input: {
504
+ forge: string;
505
+ number: number;
506
+ title: string;
507
+ url: string;
508
+ body?: string | null;
509
+ projectPath?: string;
510
+ }): string {
511
+ const lines = [
512
+ `${formatForgeLabel(input.forge)} Issue ${formatIssueNumber(input.forge, input.number)}: ${input.title}`,
513
+ input.url,
514
+ ];
515
+ if (input.projectPath) {
516
+ lines.push(`Project: ${input.projectPath}`);
517
+ }
518
+ if (input.body) {
519
+ lines.push("", input.body);
520
+ }
521
+ return lines.join("\n");
522
+ }
523
+
524
+ function formatForgeLabel(forge: string): string {
525
+ return getForgeDefinitionOrNeutral(forge).displayName;
526
+ }
527
+
528
+ function formatChangeRequestAbbrev(forge: string): string {
529
+ return getForgeDefinitionOrNeutral(forge).changeRequestAbbrev;
530
+ }
531
+
532
+ function formatChangeRequestNumber(forge: string, number: number): string {
533
+ return `${getForgeDefinitionOrNeutral(forge).changeRequestNumberPrefix}${number}`;
534
+ }
535
+
536
+ function formatIssueNumber(forge: string, number: number): string {
537
+ return `${getForgeDefinitionOrNeutral(forge).issueNumberPrefix}${number}`;
538
+ }
539
+
540
+ function padLineNumber(lineNumber: number | null): string {
541
+ return (lineNumber?.toString() ?? "-").padStart(2);
542
+ }
543
+
544
+ function promptPayload(input: SessionPromptInput): OmpPromptPayload {
545
+ if (input.prompt.outputSchema !== undefined || input.prompt.clearPendingPermissions) {
546
+ throw new OmpPublicError("OMP does not support structured output or permission controls");
547
+ }
548
+ if (input.prompt.input.type === "command") {
549
+ const name = input.prompt.input.name.trim();
550
+ if (!isSafeCommandName(name)) throw new OmpPublicError("Invalid OMP command name");
551
+ const argumentsText = input.prompt.input.arguments.trim();
552
+ const text = `/${name}${argumentsText ? ` ${argumentsText}` : ""}`;
553
+ if (utf8Bytes(text) > MAX_PROMPT_TEXT_LENGTH)
554
+ throw new OmpPublicError("OMP command is too large");
555
+ return { text, images: [], commandName: name };
556
+ }
557
+ if (input.prompt.input.content.length > MAX_PROMPT_PARTS) {
558
+ throw new OmpPublicError("OMP prompt has too many content parts");
559
+ }
560
+ const parts: string[] = [];
561
+ const images: OmpImage[] = [];
562
+ let length = 0;
563
+ const appendText = (text: string) => {
564
+ length += utf8Bytes(text) + (parts.length > 0 ? 2 : 0);
565
+ if (length > MAX_PROMPT_TEXT_LENGTH) throw new OmpPublicError("OMP prompt is too large");
566
+ parts.push(text);
567
+ };
568
+ for (const part of input.prompt.input.content) {
569
+ if (part.type === "text") {
570
+ appendText(part.text);
571
+ continue;
572
+ }
573
+ if (part.type === "image") {
574
+ if (!isValidImagePayload(part.data, part.mimeType, 8 * 1024 * 1024)) {
575
+ throw new OmpPublicError("OMP prompt image is invalid");
576
+ }
577
+ images.push({ type: "image", data: part.data, mimeType: part.mimeType });
578
+ continue;
579
+ }
580
+ appendText(renderPromptAttachmentAsText(part));
581
+ }
582
+ const text = parts.join("\n\n").trim();
583
+ if (!text && images.length === 0) throw new OmpPublicError("OMP prompt cannot be empty");
584
+ return { text, images };
585
+ }
586
+
587
+ function inlinePromptFrameBytes(payload: OmpPromptPayload, delivery: "prompt" | "steer"): number {
588
+ const images = payload.images.map(({ type, mimeType }) => ({ type, data: "", mimeType }));
589
+ const frame = {
590
+ type: delivery,
591
+ message: payload.text,
592
+ ...(images.length > 0 ? { images } : {}),
593
+ ...(delivery === "prompt" ? { id: "" } : {}),
594
+ };
595
+ const requestIdBytes = delivery === "prompt" ? RPC_REQUEST_ID_BYTES : 0;
596
+ return (
597
+ utf8Bytes(JSON.stringify(frame)) +
598
+ requestIdBytes +
599
+ payload.images.reduce((total, image) => total + image.data.length, 0) +
600
+ 1
601
+ );
602
+ }
603
+
604
+ function slashCommandName(text: string): string | undefined {
605
+ if (!text.startsWith("/")) return undefined;
606
+ const body = text.slice(1);
607
+ if (!body) return undefined;
608
+ const firstWhitespace = body.search(/\s/u);
609
+ const name = firstWhitespace === -1 ? body : body.slice(0, firstWhitespace);
610
+ return name || undefined;
611
+ }
612
+
613
+ function nativeEntryId(message: OmpMessage): string | undefined {
614
+ return message.entryId;
615
+ }
616
+
617
+ type AgentEndOutcome = "completed" | "failed" | "canceled";
618
+ type AssistantTerminalStatus = AgentEndOutcome | "unavailable";
619
+
620
+ function assistantTerminalOutcome(
621
+ message: Extract<OmpMessage, { role: "assistant" }>,
622
+ ): AgentEndOutcome {
623
+ const stopReason = message.stopReason?.toLowerCase();
624
+ if (stopReason === "aborted" || stopReason === "canceled" || stopReason === "cancelled") {
625
+ return "canceled";
626
+ }
627
+ return stopReason === "error" || message.errorMessage ? "failed" : "completed";
628
+ }
629
+
630
+ function lastAssistantStatus(
631
+ messages: readonly OmpMessage[],
632
+ startIndex = 0,
633
+ ): AssistantTerminalStatus {
634
+ for (let index = messages.length - 1; index >= startIndex; index -= 1) {
635
+ const message = messages[index];
636
+ if (message?.role !== "assistant") continue;
637
+ return assistantTerminalOutcome(message);
638
+ }
639
+ return "unavailable";
640
+ }
641
+
642
+ function terminalOutcome(
643
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
644
+ turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
645
+ ): AgentEndOutcome | undefined {
646
+ const messages = event.messages;
647
+ if (
648
+ messages !== undefined &&
649
+ (event.messageCount === undefined || messages.length >= event.messageCount)
650
+ ) {
651
+ const status = lastAssistantStatus(messages);
652
+ return status === "unavailable" ? "completed" : status;
653
+ }
654
+ if (event.messageCount === 0) return "completed";
655
+ if (
656
+ turn.lastCompletedAssistantOutcome !== undefined &&
657
+ (event.messageCount === undefined || turn.completedMessageCount >= event.messageCount)
658
+ ) {
659
+ return turn.lastCompletedAssistantOutcome;
660
+ }
661
+ return undefined;
662
+ }
663
+
664
+ function historyTerminalOutcome(
665
+ messages: readonly OmpMessage[],
666
+ declaredCount: number,
667
+ turn: Pick<
668
+ ActiveTurn,
669
+ | "streamedMessageEntryIds"
670
+ | "streamedMessageIdentityComplete"
671
+ | "lastCompletedAssistantOutcome"
672
+ | "lastCompletedAssistantEntryId"
673
+ >,
674
+ retainedMessages: readonly OmpMessage[],
675
+ ): AgentEndOutcome | undefined {
676
+ if (
677
+ messages.length < declaredCount ||
678
+ !turn.streamedMessageIdentityComplete ||
679
+ turn.streamedMessageEntryIds.length === 0
680
+ ) {
681
+ return undefined;
682
+ }
683
+ const startIndex = messages.length - declaredCount;
684
+ let historyIndex = startIndex;
685
+ for (const entryId of turn.streamedMessageEntryIds) {
686
+ while (historyIndex < messages.length) {
687
+ const historyMessage = messages[historyIndex];
688
+ if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
689
+ historyIndex += 1;
690
+ }
691
+ if (historyIndex >= messages.length) return undefined;
692
+ const correlated = messages[historyIndex];
693
+ if (
694
+ entryId === turn.lastCompletedAssistantEntryId &&
695
+ (correlated?.role !== "assistant" ||
696
+ assistantTerminalOutcome(correlated) !== turn.lastCompletedAssistantOutcome)
697
+ )
698
+ return undefined;
699
+ historyIndex += 1;
700
+ }
701
+ historyIndex = startIndex;
702
+ for (const retained of retainedMessages) {
703
+ const entryId = nativeEntryId(retained);
704
+ if (!entryId) return undefined;
705
+ while (historyIndex < messages.length) {
706
+ const historyMessage = messages[historyIndex];
707
+ if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
708
+ historyIndex += 1;
709
+ }
710
+ if (historyIndex >= messages.length) return undefined;
711
+ const correlated = messages[historyIndex];
712
+ if (!correlated || correlated.role !== retained.role) return undefined;
713
+ if (
714
+ retained.role === "assistant" &&
715
+ correlated.role === "assistant" &&
716
+ assistantTerminalOutcome(retained) !== assistantTerminalOutcome(correlated)
717
+ )
718
+ return undefined;
719
+ historyIndex += 1;
720
+ }
721
+ const status = lastAssistantStatus(messages, startIndex);
722
+ return status === "unavailable" ? undefined : status;
723
+ }
724
+
725
+ function unknownTerminalOutcomeError(
726
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
727
+ turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
728
+ ): string {
729
+ const retainedMessages = event.messages?.length ?? 0;
730
+ const retainedStatus = event.messages ? lastAssistantStatus(event.messages) : "unavailable";
731
+ const lastStatus =
732
+ retainedStatus === "unavailable"
733
+ ? (turn.lastCompletedAssistantOutcome ?? "unavailable")
734
+ : retainedStatus;
735
+ return (
736
+ "OMP agent_end omitted terminal messages; outcome is unknown " +
737
+ `(declaredCount=${event.messageCount ?? "unavailable"}, ` +
738
+ `observedCount=${turn.completedMessageCount}, ` +
739
+ `retainedTerminalMessages=${retainedMessages}, lastAssistantStatus=${lastStatus})`
740
+ );
741
+ }
742
+
743
+ function isNativeTurnActivity(event: OmpRpcEvent): boolean {
744
+ if (
745
+ event.type === "agent_start" ||
746
+ event.type === "turn_start" ||
747
+ event.type === "turn_end" ||
748
+ event.type === "agent_end" ||
749
+ event.type === "auto_compaction_start" ||
750
+ event.type === "auto_compaction_end"
751
+ ) {
752
+ return true;
753
+ }
754
+ if (
755
+ event.type === "message_start" ||
756
+ event.type === "message_update" ||
757
+ event.type === "message_end"
758
+ ) {
759
+ return event.message.role === "assistant";
760
+ }
761
+ return event.type.startsWith("tool_execution_");
762
+ }
763
+ function isRuntimeConfigEvent(event: OmpRpcEvent): boolean {
764
+ return (
765
+ event.type === "model_changed" ||
766
+ event.type === "thinking_level_changed" ||
767
+ event.type === "retry_fallback_applied" ||
768
+ event.type === "retry_fallback_succeeded"
769
+ );
770
+ }
771
+
772
+ function isPassiveUiMethod(method: string): boolean {
773
+ return (
774
+ method === "cancel" ||
775
+ method === "notify" ||
776
+ method === "open_url" ||
777
+ method === "setStatus" ||
778
+ method === "setWidget" ||
779
+ method === "setTitle" ||
780
+ method === "set_editor_text"
781
+ );
782
+ }
783
+ function createActiveTurn(
784
+ clientMessageId: string,
785
+ text: string,
786
+ generation: number,
787
+ terminalOwnershipRequired: boolean,
788
+ manualCompaction = false,
789
+ ): ActiveTurn {
790
+ return {
791
+ turnId: randomUUID(),
792
+ clientMessageId,
793
+ agentInvoked: undefined,
794
+ generation,
795
+ promptResultEmitted: false,
796
+ started: false,
797
+ terminal: false,
798
+ awaitingPermissionEvidence: false,
799
+ interrupted: false,
800
+ starting: true,
801
+ nativeActivity: false,
802
+ userEchoObserved: false,
803
+ localOnlyDisabled: false,
804
+ localOnlyEligible: false,
805
+ usageSampleFloor: 0,
806
+ agentEndPending: false,
807
+ terminalizing: false,
808
+ manualCompactionPending: manualCompaction,
809
+ manualCompaction,
810
+ activitySequence: 0,
811
+ acknowledged: false,
812
+ terminalOwnershipEvidence: false,
813
+ replayingBufferedEvents: false,
814
+ bufferedTerminalOwnershipEvidence: false,
815
+ terminalOwnershipRequired,
816
+ steersInFlight: 0,
817
+ steerReady: Promise.withResolvers<void>(),
818
+ userCorrelationActive: false,
819
+ userLookups: new Set(),
820
+ userEchoes: [],
821
+ completedMessageCount: 0,
822
+ bufferedEvents: [],
823
+ streamedMessageEntryIds: [],
824
+ streamedMessageIdentityComplete: true,
825
+ pendingUsers: [
826
+ {
827
+ clientMessageId,
828
+ text,
829
+ accepted: true,
830
+ fallbackOnFinish: true,
831
+ bufferedEchoes: [],
832
+ },
833
+ ],
834
+ };
835
+ }
836
+
837
+ export class OmpProviderSession {
838
+ readonly id: string;
839
+ readonly cwd: string;
840
+
841
+ private readonly projector: OmpTimelineProjector;
842
+ private readonly subsessions: OmpSubsessionProjector | null;
843
+ private unsubscribe: () => void = () => {};
844
+ private activeTurn: ActiveTurn | null = null;
845
+ private closed = false;
846
+ private disposalPromise: Promise<void> | null = null;
847
+ private sessionClosedPublished = false;
848
+ private readyPublished = false;
849
+ private readonly emittedEntryIds = new BoundedStringSet(MAX_TRACKED_ENTRY_IDS);
850
+ private readonly seenEntryIds = new BoundedStringSet(MAX_TRACKED_ENTRY_IDS);
851
+ private branchWatermarkValid = true;
852
+ private readonly branchEntryIds = new Set<string>();
853
+ private readonly unclaimedBranchEntries: Array<{ entryId: string; text: string }> = [];
854
+ private readonly scheduler: OmpTimelineScheduler;
855
+ private readonly dataFilter: OmpPublicDataSerializer;
856
+ private readonly nativeModelsByPublicId: ReadonlyMap<string, OmpModel>;
857
+ private readonly lifetime = new AbortController();
858
+ private generation = 0;
859
+ private runtimeDead: string | null = null;
860
+ private runtimeDisposal: Promise<void> | null = null;
861
+ private hostToolsDisposal: Promise<void> | null = null;
862
+ private recoveryPromise: Promise<void> | null = null;
863
+ private configRefreshInFlight: Promise<void> | null = null;
864
+ private configRefreshDirty = false;
865
+ private configRefreshAttempts = 0;
866
+ private configRefreshRetryHandle: unknown | null = null;
867
+ private configRefreshRetryResolve: (() => void) | null = null;
868
+ private configMutationInFlight = false;
869
+ private configRevision = 0;
870
+ private recoveryUsesNativeConfig = false;
871
+ private activeAbort: PendingAbort | null = null;
872
+ private usageEpoch = 0;
873
+ private usageSequence = 0;
874
+ private usageSample: {
875
+ turn: ActiveTurn;
876
+ generation: number;
877
+ runtime: OmpRuntimeSession;
878
+ epoch: number;
879
+ sequence: number;
880
+ promise: Promise<OmpSessionState | undefined>;
881
+ } | null = null;
882
+ private activeCompaction: ActiveCompaction | null = null;
883
+ private discardedCompactionEnds = 0;
884
+ private lastUsage: ProviderUsage | null = null;
885
+ private revertInFlight = false;
886
+ private runtimeTurnCompleted = false;
887
+ private commandCatalog: OmpAvailableCommand[];
888
+ private permissionSequence = 0;
889
+ private readonly permissionNamespace = randomUUID();
890
+ private readonly pendingPermissions = new Map<string, PendingPermission>();
891
+ private readonly inFlightPermissions = new Map<string, PendingPermission>();
892
+ private pendingFreeformSelection: PendingFreeformSelection | null = null;
893
+ private readonly pendingToolPermissions = new Map<string, PendingToolPermission>();
894
+ private readonly inFlightToolPermissions = new Map<string, PendingToolPermission>();
895
+ private readonly resolvedToolApprovalIds = new BoundedStringSet(MAX_TRACKED_ENTRY_IDS);
896
+
897
+ private constructor(
898
+ id: string,
899
+ private runtime: OmpRuntimeSession,
900
+ private readonly runtimeFactory: OmpRuntime,
901
+ private recoveryOptions: OmpRecoveryOptions,
902
+ private readonly hostTools: OmpHostToolsBridge,
903
+ private nativeSessionId: string,
904
+ private readonly nativeSessionFile: string | undefined,
905
+ private readonly config: ProviderSessionConfig,
906
+ private configState: ProviderConfigState,
907
+ outputRedactionValues: readonly string[],
908
+ nativeModelsByPublicId: ReadonlyMap<string, OmpModel>,
909
+ private readonly capabilities: readonly string[],
910
+ private readonly slashCommands: Set<string>,
911
+ commandCatalog: OmpAvailableCommand[],
912
+ private commandDiscoveryAvailable: boolean,
913
+ private readonly emit: Emit,
914
+ private readonly replayHistoryOnOpen: boolean,
915
+ private readonly persistSession: boolean,
916
+ private readonly replayTimeoutMs: number,
917
+ private readonly transitionNativeSession: NativeSessionTransition,
918
+ private readonly quarantineRewindCleanup: RewindCleanupQuarantine,
919
+ private readonly retireRewindSession: RewindSessionRetirement,
920
+ scheduler: OmpTimelineScheduler = defaultOmpTimelineScheduler,
921
+ ) {
922
+ this.id = id;
923
+ this.cwd = config.cwd;
924
+ this.scheduler = scheduler;
925
+ this.dataFilter = new OmpPublicDataSerializer(outputRedactionValues);
926
+ this.nativeModelsByPublicId = nativeModelsByPublicId;
927
+ this.commandCatalog = commandCatalog;
928
+ this.hostTools.onFatal(() => this.handleRuntimeFailure());
929
+ this.projector = new OmpTimelineProjector(
930
+ id,
931
+ emit,
932
+ scheduler,
933
+ outputRedactionValues,
934
+ capabilities.includes("session.revert.conversation"),
935
+ capabilities.includes("timeline.plugin"),
936
+ hostTools.labels,
937
+ );
938
+ this.subsessions = capabilities.includes("session.subsession")
939
+ ? new OmpSubsessionProjector(
940
+ id,
941
+ persistSession ? `persisted:${nativeSessionId}` : `ephemeral:${id}`,
942
+ nativeSessionFile,
943
+ config.cwd,
944
+ emit,
945
+ scheduler,
946
+ () => this.resumeDeferredAgentEnd(),
947
+ outputRedactionValues,
948
+ capabilities.includes("timeline.plugin"),
949
+ )
950
+ : null;
951
+ this.bindRuntime(runtime);
952
+ }
953
+ get persistenceSessionId(): string | undefined {
954
+ return this.persistSession ? this.nativeSessionId : undefined;
955
+ }
956
+ async openPaseoBrowser(url: string): Promise<void> {
957
+ if (this.closed) throw new OmpPublicError("The OMP session is closed");
958
+ await this.hostTools.openPaseoBrowser(url);
959
+ }
960
+ setBrowserAuthorizationIssuer(issue: ((url: string) => string | undefined) | null): void {
961
+ this.projector.setBrowserAuthorizationIssuer(issue);
962
+ }
963
+
964
+ private readonly imageMaterializer = new OmpImageMaterializer();
965
+ static async open(
966
+ input: SessionOpenInput,
967
+ runtime: OmpRuntime,
968
+ capabilities: readonly string[],
969
+ emit: Emit,
970
+ transitionNativeSession: NativeSessionTransition,
971
+ quarantineRewindCleanup: RewindCleanupQuarantine,
972
+ retireRewindSession: RewindSessionRetirement,
973
+ scheduler?: OmpTimelineScheduler,
974
+ replayTimeoutMs = REPLAY_TIMEOUT_MS,
975
+ signal?: AbortSignal,
976
+ environment?: NodeJS.ProcessEnv,
977
+ mcpConnector?: OmpMcpConnector,
978
+ mcpInitializationTimeoutMs?: number,
979
+ ): Promise<OmpProviderSession> {
980
+ const resumeSessionId = ompPersistenceSessionId(input);
981
+ if (resumeSessionId && !input.config.persist) {
982
+ throw new OmpPublicError("OMP persisted sessions require persist: true");
983
+ }
984
+ if (input.history === "replay" && !resumeSessionId) {
985
+ throw new OmpPublicError("OMP history replay requires persisted session identity");
986
+ }
987
+ if (input.history === "skip" && resumeSessionId) {
988
+ throw new OmpPublicError("OMP persisted sessions require history replay");
989
+ }
990
+ const effectiveConfig: ProviderSessionConfig = { ...input.config };
991
+ const normalizedConfig = normalizeOmpSessionConfig(
992
+ effectiveConfig,
993
+ capabilities.includes("permission"),
994
+ );
995
+ const configuredRedactionValues = configuredOutputRedactionValues(
996
+ normalizedConfig.outputRedaction ?? "none",
997
+ normalizedConfig.env,
998
+ effectiveConfig.mcpServers,
999
+ );
1000
+ const persistedDescriptor = resumeSessionId
1001
+ ? await authorizeNativeSession(
1002
+ runtime,
1003
+ resumeSessionId,
1004
+ input.config.cwd,
1005
+ normalizedConfig.sessionDir,
1006
+ )
1007
+ : undefined;
1008
+ validateOmpHostToolConfig(effectiveConfig);
1009
+ if (input.config.title && utf8Bytes(input.config.title) > 256) {
1010
+ throw new OmpPublicError("OMP session title is too large");
1011
+ }
1012
+ const startOptions: OmpStartOptions = {
1013
+ ...normalizedConfig,
1014
+ // Model selection is authorized only after this runtime reports its exact catalog.
1015
+ ...(resumeSessionId
1016
+ ? { thinkingOption: undefined, systemPrompt: undefined, resumeSessionId }
1017
+ : {}),
1018
+ signal,
1019
+ environment,
1020
+ };
1021
+ buildOmpSpawnRequest(startOptions);
1022
+ const hostTools = await OmpHostToolsBridge.open(effectiveConfig, {
1023
+ connectMcp: mcpConnector,
1024
+ signal,
1025
+ initializationTimeoutMs: mcpInitializationTimeoutMs,
1026
+ });
1027
+ let native: OmpRuntimeSession | undefined;
1028
+ let cleanupNativeSessionId: string | undefined;
1029
+ let unsubscribeBootstrap = () => {};
1030
+ let bootstrapConfigRevision = 0;
1031
+ try {
1032
+ native = await runtime.startSession(startOptions);
1033
+ const outputRedactionValues =
1034
+ normalizedConfig.outputRedaction === "configured-values"
1035
+ ? [...configuredRedactionValues, ...(native.inheritedRedactionValues ?? [])]
1036
+ : configuredRedactionValues;
1037
+ unsubscribeBootstrap = native.onEvent((event) => {
1038
+ if (event.type === "host_tool_call" || event.type === "host_tool_cancel") {
1039
+ hostTools.handle(event);
1040
+ return;
1041
+ }
1042
+ if (isRuntimeConfigEvent(event)) bootstrapConfigRevision += 1;
1043
+ });
1044
+ await hostTools.bind(native);
1045
+ const [initialState, nativeModels, commandDiscovery] = await Promise.all([
1046
+ native.getState(),
1047
+ native.getAvailableModels(),
1048
+ native.getAvailableCommands().then(
1049
+ (commands) => ({ available: true, commands }),
1050
+ () => ({ available: false, commands: [] }),
1051
+ ),
1052
+ ]);
1053
+ if (effectiveConfig.persist && !native.canReplayHistory) {
1054
+ throw new OmpPublicError("OMP session persistence requires negotiated RPC protocol v2");
1055
+ }
1056
+ if (capabilities.includes("session.revert.conversation") && !native.canReplayHistory) {
1057
+ throw new OmpPublicError("OMP conversation rewind requires negotiated RPC protocol v2");
1058
+ }
1059
+ let state = initialState;
1060
+ if (effectiveConfig.persist || resumeSessionId) {
1061
+ cleanupNativeSessionId = validateNativeSessionId(initialState.sessionId);
1062
+ }
1063
+ if (resumeSessionId && initialState.sessionId !== resumeSessionId) {
1064
+ throw new OmpPublicError("OMP resumed a different native session");
1065
+ }
1066
+ const models = mapOmpModels(nativeModels, new OmpPublicDataSerializer(outputRedactionValues));
1067
+ const nativeModelsByPublicId = new Map(
1068
+ nativeModels.map((model) => [ompModelId(model), model] as const),
1069
+ );
1070
+ if (!resumeSessionId && input.config.model) {
1071
+ const selected = nativeModelsByPublicId.get(input.config.model);
1072
+ if (!selected) {
1073
+ throw new OmpPublicError("OMP model is not advertised by the configured session runtime");
1074
+ }
1075
+ if (state.model?.provider !== selected.provider || state.model.id !== selected.id) {
1076
+ await native.setModel(selected.provider, selected.id);
1077
+ state = await native.getState();
1078
+ }
1079
+ }
1080
+ const reconciledConfigRevision = bootstrapConfigRevision;
1081
+ state = await native.getState();
1082
+ const currentModel = state.model
1083
+ ? nativeModels.find(
1084
+ (model) => model.provider === state.model?.provider && model.id === state.model.id,
1085
+ )
1086
+ : undefined;
1087
+ if (state.model && !currentModel) {
1088
+ throw new OmpPublicError("OMP runtime selected an unadvertised model");
1089
+ }
1090
+ const thinkingOptions = thinkingForModel(currentModel);
1091
+ const committedThinkingLevel = applicableThinkingLevel(currentModel, state.thinkingLevel);
1092
+ if (
1093
+ !resumeSessionId &&
1094
+ input.config.thinkingOption !== undefined &&
1095
+ !thinkingOptions.some((option) => option.id === input.config.thinkingOption)
1096
+ ) {
1097
+ throw new OmpPublicError("OMP thinking level is unavailable for the selected model");
1098
+ }
1099
+ if (
1100
+ committedThinkingLevel &&
1101
+ !thinkingOptions.some((option) => option.id === committedThinkingLevel)
1102
+ ) {
1103
+ throw new OmpPublicError("OMP runtime selected an unsupported thinking level");
1104
+ }
1105
+ const configState: ProviderConfigState = {
1106
+ ...(state.model ? { model: ompModelId(state.model) } : {}),
1107
+ mode: normalizedConfig.mode ?? "full",
1108
+ ...(committedThinkingLevel ? { thinkingOption: committedThinkingLevel } : {}),
1109
+ models,
1110
+ // OMP fixes approval mode at process launch. Publish only the selected mode so
1111
+ // Paseo shows the security state without offering unsupported transitions.
1112
+ modes: [fixedSessionMode(normalizedConfig.mode)],
1113
+ thinkingOptions,
1114
+ settings: [],
1115
+ };
1116
+ const { signal: _signal, ...recoveryTemplate } = startOptions;
1117
+ const recoveryOptions = withCommittedOmpSelection(recoveryTemplate, {
1118
+ model: state.model ? nativeOmpModelId(state.model) : undefined,
1119
+ thinkingOption: committedThinkingLevel,
1120
+ });
1121
+ if (!hostTools.isBoundTo(native)) {
1122
+ throw new Error("OMP host tool bridge detached during session initialization");
1123
+ }
1124
+ unsubscribeBootstrap();
1125
+ unsubscribeBootstrap = () => {};
1126
+ let sessionCapabilities = capabilities;
1127
+ if (capabilities.includes("session.subsession")) {
1128
+ try {
1129
+ await native.setSubagentSubscription("events");
1130
+ } catch {
1131
+ sessionCapabilities = capabilities.filter(
1132
+ (capability) => capability !== "session.subsession",
1133
+ );
1134
+ }
1135
+ }
1136
+ const session = new OmpProviderSession(
1137
+ input.sessionId,
1138
+ native,
1139
+ runtime,
1140
+ recoveryOptions,
1141
+ hostTools,
1142
+ state.sessionId,
1143
+ persistedDescriptor?.transcriptFile ?? state.sessionFile,
1144
+ effectiveConfig,
1145
+ configState,
1146
+ outputRedactionValues,
1147
+ nativeModelsByPublicId,
1148
+ sessionCapabilities,
1149
+ new Set([
1150
+ ...OMP_BUILTIN_COMMANDS.map((command) => command.name),
1151
+ ...commandDiscovery.commands.flatMap((command) => [
1152
+ command.name,
1153
+ ...(command.aliases ?? []),
1154
+ ]),
1155
+ ]),
1156
+ commandDiscovery.commands,
1157
+ commandDiscovery.available,
1158
+ emit,
1159
+ input.history === "replay",
1160
+ effectiveConfig.persist,
1161
+ replayTimeoutMs,
1162
+ transitionNativeSession,
1163
+ quarantineRewindCleanup,
1164
+ retireRewindSession,
1165
+ scheduler,
1166
+ );
1167
+
1168
+ if (bootstrapConfigRevision !== reconciledConfigRevision) {
1169
+ session.configRefreshDirty = true;
1170
+ }
1171
+ return session;
1172
+ } catch (error) {
1173
+ unsubscribeBootstrap();
1174
+ const directCleanup = [
1175
+ Promise.resolve().then(() => hostTools.close()),
1176
+ ...(native ? [native] : []).map((session) => Promise.resolve().then(() => session.close())),
1177
+ ];
1178
+ if (isOmpCleanupFailure(error)) {
1179
+ throw new OmpCleanupFailure(
1180
+ "OMP session initialization cleanup pending",
1181
+ settleSessionCleanup([error.cleanup, ...directCleanup]),
1182
+ cleanupNativeSessionId ?? error.nativeSessionId,
1183
+ );
1184
+ }
1185
+ const directResults = await Promise.allSettled(directCleanup);
1186
+ const nestedCleanup: Promise<void>[] = [];
1187
+ const cleanupFailures: unknown[] = [];
1188
+ for (const result of directResults) {
1189
+ if (result.status !== "rejected") continue;
1190
+ if (isOmpCleanupFailure(result.reason)) nestedCleanup.push(result.reason.cleanup);
1191
+ else cleanupFailures.push(result.reason);
1192
+ }
1193
+ if (nestedCleanup.length > 0 || cleanupFailures.length > 0) {
1194
+ const failed =
1195
+ cleanupFailures.length > 0
1196
+ ? [
1197
+ Promise.reject(
1198
+ new AggregateError(cleanupFailures, "OMP session initialization cleanup failed"),
1199
+ ),
1200
+ ]
1201
+ : [];
1202
+ throw new OmpCleanupFailure(
1203
+ "OMP session initialization cleanup pending",
1204
+ settleSessionCleanup([...nestedCleanup, ...failed]),
1205
+ cleanupNativeSessionId,
1206
+ );
1207
+ }
1208
+ throw error;
1209
+ }
1210
+ }
1211
+
1212
+ async publishOpened(requestId: string): Promise<void> {
1213
+ this.emit({
1214
+ type: "session.opened",
1215
+ requestId,
1216
+ sessionId: this.id,
1217
+ capabilities: this.capabilities,
1218
+ restoration: "core",
1219
+ cwd: this.cwd,
1220
+ ...(this.persistSession
1221
+ ? { persistence: { version: 1, data: { sessionId: this.nativeSessionId } } }
1222
+ : {}),
1223
+ ...(this.config.title ? { title: this.dataFilter.text(this.config.title, 256) } : {}),
1224
+ });
1225
+ this.emit({ type: "session.config", sessionId: this.id, config: this.configState });
1226
+ if (this.replayHistoryOnOpen) await this.replayHistory(true);
1227
+ this.publishCommands(this.commandCatalog);
1228
+ this.emit({ type: "session.ready", requestId, sessionId: this.id });
1229
+ this.readyPublished = true;
1230
+ if (this.configRefreshDirty) this.scheduleCommittedConfigRefresh();
1231
+ }
1232
+
1233
+ private usageFrom(
1234
+ state: OmpSessionState | undefined,
1235
+ stats: OmpSessionStats | undefined,
1236
+ ): ProviderUsage | undefined {
1237
+ if (!state?.contextUsage && !stats) return undefined;
1238
+ const stateContext = state?.contextUsage;
1239
+ const statsContext = stats?.contextUsage;
1240
+ const modelCapacity = state?.model?.contextWindow;
1241
+ const inputTokens = stats?.tokens?.input;
1242
+ const cachedInputTokens = stats?.tokens?.cacheRead;
1243
+ const outputTokens = stats?.tokens?.output;
1244
+ const totalCostUsd = stats?.cost;
1245
+ const contextTokens =
1246
+ typeof stateContext?.tokens === "number"
1247
+ ? stateContext.tokens
1248
+ : typeof statsContext?.tokens === "number"
1249
+ ? statsContext.tokens
1250
+ : undefined;
1251
+ const contextWindow =
1252
+ typeof stateContext?.contextWindow === "number" && stateContext.contextWindow > 0
1253
+ ? stateContext.contextWindow
1254
+ : typeof statsContext?.contextWindow === "number" && statsContext.contextWindow > 0
1255
+ ? statsContext.contextWindow
1256
+ : typeof modelCapacity === "number" && modelCapacity > 0
1257
+ ? modelCapacity
1258
+ : undefined;
1259
+ const usage: ProviderUsage = {
1260
+ ...(typeof inputTokens === "number" ? { inputTokens } : {}),
1261
+ ...(typeof cachedInputTokens === "number" ? { cachedInputTokens } : {}),
1262
+ ...(typeof outputTokens === "number" ? { outputTokens } : {}),
1263
+ ...(typeof totalCostUsd === "number" ? { totalCostUsd } : {}),
1264
+ ...(contextTokens !== undefined ? { contextWindowUsedTokens: contextTokens } : {}),
1265
+ ...(contextWindow !== undefined ? { contextWindowMaxTokens: contextWindow } : {}),
1266
+ };
1267
+ return Object.keys(usage).length > 0 ? usage : undefined;
1268
+ }
1269
+
1270
+ private ownsUsageSample(
1271
+ turn: ActiveTurn,
1272
+ generation: number,
1273
+ runtime: OmpRuntimeSession,
1274
+ ): boolean {
1275
+ return (
1276
+ !this.closed &&
1277
+ !this.runtimeDead &&
1278
+ !turn.terminal &&
1279
+ generation === this.generation &&
1280
+ runtime === this.runtime &&
1281
+ this.activeTurn === turn
1282
+ );
1283
+ }
1284
+
1285
+ private publishUsageSnapshot(
1286
+ turn: ActiveTurn,
1287
+ minimumEpoch = this.usageEpoch,
1288
+ minimumSequence = turn.usageSampleFloor,
1289
+ ): Promise<OmpSessionState | undefined> {
1290
+ const generation = turn.generation;
1291
+ const runtime = this.runtime;
1292
+ if (!this.ownsUsageSample(turn, generation, runtime)) return Promise.resolve(undefined);
1293
+ const current = this.usageSample;
1294
+ if (current) {
1295
+ if (
1296
+ current.turn === turn &&
1297
+ current.generation === generation &&
1298
+ current.runtime === runtime &&
1299
+ current.epoch >= minimumEpoch &&
1300
+ current.sequence >= minimumSequence
1301
+ ) {
1302
+ return current.promise;
1303
+ }
1304
+ return current.promise.then(() => {
1305
+ if (!this.ownsUsageSample(turn, generation, runtime)) return undefined;
1306
+ return this.publishUsageSnapshot(turn, minimumEpoch, minimumSequence);
1307
+ });
1308
+ }
1309
+ const epoch = this.usageEpoch;
1310
+ const sequence = ++this.usageSequence;
1311
+ const promise = Promise.allSettled([runtime.getState(), runtime.getSessionStats()]).then(
1312
+ ([stateResult, statsResult]) => {
1313
+ if (
1314
+ !this.ownsUsageSample(turn, generation, runtime) ||
1315
+ epoch !== this.usageEpoch ||
1316
+ epoch < minimumEpoch ||
1317
+ sequence < turn.usageSampleFloor ||
1318
+ sequence < minimumSequence
1319
+ ) {
1320
+ return undefined;
1321
+ }
1322
+ const state = stateResult.status === "fulfilled" ? stateResult.value : undefined;
1323
+ const stats = statsResult.status === "fulfilled" ? statsResult.value : undefined;
1324
+ const usage = this.usageFrom(state, stats);
1325
+ if (usage) {
1326
+ this.lastUsage = usage;
1327
+ this.emit({ type: "session.usage", sessionId: this.id, turnId: turn.turnId, usage });
1328
+ }
1329
+ return state;
1330
+ },
1331
+ );
1332
+ this.usageSample = { turn, generation, runtime, epoch, sequence, promise };
1333
+ void promise.finally(() => {
1334
+ if (this.usageSample?.promise === promise) this.usageSample = null;
1335
+ });
1336
+ return promise;
1337
+ }
1338
+
1339
+ private async boundedUsageSnapshot(
1340
+ turn: ActiveTurn,
1341
+ timeoutMs: number,
1342
+ ): Promise<OmpSessionState | undefined> {
1343
+ const timeout = Promise.withResolvers<undefined>();
1344
+ const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
1345
+ try {
1346
+ return await Promise.race([this.publishUsageSnapshot(turn), timeout.promise]);
1347
+ } finally {
1348
+ this.scheduler.clear(timer);
1349
+ }
1350
+ }
1351
+
1352
+ private async boundedTerminalState(
1353
+ turn: ActiveTurn,
1354
+ timeoutMs: number,
1355
+ ): Promise<OmpSessionState | undefined> {
1356
+ const generation = turn.generation;
1357
+ const runtime = this.runtime;
1358
+ const timeout = Promise.withResolvers<undefined>();
1359
+ const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
1360
+ try {
1361
+ const state = await Promise.race([runtime.getState(), timeout.promise]);
1362
+ return this.ownsUsageSample(turn, generation, runtime) ? state : undefined;
1363
+ } catch {
1364
+ return undefined;
1365
+ } finally {
1366
+ this.scheduler.clear(timer);
1367
+ }
1368
+ }
1369
+
1370
+ private isActiveTurn(turn: ActiveTurn): boolean {
1371
+ return (
1372
+ !this.closed &&
1373
+ !this.runtimeDead &&
1374
+ !turn.terminal &&
1375
+ !turn.terminalizing &&
1376
+ !turn.agentEndPending &&
1377
+ turn.generation === this.generation &&
1378
+ this.activeTurn === turn
1379
+ );
1380
+ }
1381
+
1382
+ private scheduleUsagePoll(turn: ActiveTurn, delayMs = USAGE_POLL_MS): void {
1383
+ if (!this.isActiveTurn(turn) || turn.usagePollTimer !== undefined) return;
1384
+ turn.usagePollTimer = this.scheduler.set(() => {
1385
+ turn.usagePollTimer = undefined;
1386
+ this.pollUsage(turn);
1387
+ }, delayMs);
1388
+ }
1389
+
1390
+ private pollUsage(turn: ActiveTurn): void {
1391
+ if (!this.isActiveTurn(turn) || turn.usagePoll) return;
1392
+ const poll = this.publishUsageSnapshot(turn).then(() => undefined);
1393
+ turn.usagePoll = poll;
1394
+ void poll.finally(() => {
1395
+ if (turn.usagePoll === poll) turn.usagePoll = undefined;
1396
+ this.scheduleUsagePoll(turn);
1397
+ });
1398
+ }
1399
+
1400
+ private stopUsagePoll(turn: ActiveTurn): void {
1401
+ if (turn.usagePollTimer === undefined) return;
1402
+ this.scheduler.clear(turn.usagePollTimer);
1403
+ turn.usagePollTimer = undefined;
1404
+ }
1405
+
1406
+ private startCompaction(turn: ActiveTurn, trigger: "auto" | "manual", action?: string): void {
1407
+ if (this.discardedCompactionEnds > 0) {
1408
+ this.discardedCompactionEnds += 1;
1409
+ return;
1410
+ }
1411
+ const active = this.activeCompaction;
1412
+ if (active && active.trigger === trigger && active.action === action) {
1413
+ active.retrying = false;
1414
+ return;
1415
+ }
1416
+ if (active) {
1417
+ this.retireCompaction("OMP emitted overlapping compactions");
1418
+ this.discardedCompactionEnds = 2;
1419
+ return;
1420
+ }
1421
+ const operation: ActiveCompaction = {
1422
+ id: randomUUID(),
1423
+ trigger,
1424
+ turnId: turn.turnId,
1425
+ generation: turn.generation,
1426
+ action,
1427
+ retrying: false,
1428
+ preTokens: this.lastUsage?.contextWindowUsedTokens,
1429
+ };
1430
+ this.activeCompaction = operation;
1431
+ this.projector.flush(true);
1432
+ this.emit({
1433
+ type: "timeline.item",
1434
+ sessionId: this.id,
1435
+ item: {
1436
+ id: operation.id,
1437
+ type: "compaction",
1438
+ status: "loading",
1439
+ trigger,
1440
+ ...(operation.preTokens !== undefined ? { preTokens: operation.preTokens } : {}),
1441
+ },
1442
+ });
1443
+ }
1444
+
1445
+ private finishCompaction(
1446
+ state: "completed" | "failed" | "canceled" | "skipped",
1447
+ options: { tokensBefore?: number | null; message?: string } = {},
1448
+ ): void {
1449
+ const operation = this.activeCompaction;
1450
+ if (!operation) return;
1451
+ this.activeCompaction = null;
1452
+ this.projector.flush(true);
1453
+ if (state === "completed") {
1454
+ this.usageEpoch += 1;
1455
+ const staleSample = this.usageSample;
1456
+ if (staleSample && staleSample.epoch < this.usageEpoch) {
1457
+ this.usageSample = null;
1458
+ staleSample.turn.usagePoll = undefined;
1459
+ }
1460
+ this.lastUsage = null;
1461
+ }
1462
+ if (state !== "completed") {
1463
+ const defaultMessage =
1464
+ state === "failed"
1465
+ ? "OMP compaction failed"
1466
+ : state === "canceled"
1467
+ ? "OMP compaction canceled"
1468
+ : "OMP compaction skipped";
1469
+ this.emit({
1470
+ type: "timeline.item",
1471
+ sessionId: this.id,
1472
+ item: {
1473
+ id: operation.id,
1474
+ type: "notification",
1475
+ level: state === "failed" ? "error" : "info",
1476
+ message: this.dataFilter.text(options.message ?? defaultMessage, 4_096),
1477
+ },
1478
+ });
1479
+ return;
1480
+ }
1481
+ const tokensBefore = options.tokensBefore ?? operation.preTokens;
1482
+ this.emit({
1483
+ type: "timeline.item",
1484
+ sessionId: this.id,
1485
+ item: {
1486
+ id: operation.id,
1487
+ type: "compaction",
1488
+ status: "completed",
1489
+ trigger: operation.trigger,
1490
+ ...(tokensBefore !== undefined ? { preTokens: tokensBefore } : {}),
1491
+ },
1492
+ });
1493
+ }
1494
+
1495
+ private retireCompaction(message: string): void {
1496
+ const operation = this.activeCompaction;
1497
+ if (!operation) return;
1498
+ this.activeCompaction = null;
1499
+ this.emit({
1500
+ type: "timeline.item",
1501
+ sessionId: this.id,
1502
+ item: {
1503
+ id: operation.id,
1504
+ type: "compaction",
1505
+ status: "completed",
1506
+ trigger: operation.trigger,
1507
+ },
1508
+ });
1509
+ this.emit({
1510
+ type: "timeline.item",
1511
+ sessionId: this.id,
1512
+ item: { id: `${operation.id}:error`, type: "error", message },
1513
+ });
1514
+ }
1515
+
1516
+ private async replayHistory(preferPersistedTranscript = false): Promise<void> {
1517
+ if (!this.runtime.canReplayHistory) {
1518
+ throw new OmpPublicError("OMP session history cannot be replayed safely");
1519
+ }
1520
+ this.lifetime.signal.throwIfAborted();
1521
+ const replay = new AbortController();
1522
+ const onAbort = () =>
1523
+ replay.abort(new OmpPublicError("OMP session history replay was canceled"));
1524
+ this.lifetime.signal.addEventListener("abort", onAbort, { once: true });
1525
+ const timeoutHandle = setTimeout(
1526
+ () => replay.abort(new OmpPublicError("OMP session history replay timed out")),
1527
+ this.replayTimeoutMs,
1528
+ );
1529
+ try {
1530
+ let messages: OmpMessage[] | undefined;
1531
+ // OMP's RPC history is model context, which excludes failed/aborted turns. On initial
1532
+ // resume/import prefer the already authorized journal; rewinds still use the runtime's
1533
+ // in-memory branch because an uncommitted leaf move is not represented by file order.
1534
+ if (
1535
+ preferPersistedTranscript &&
1536
+ this.nativeSessionFile &&
1537
+ this.runtimeFactory.readPersistedSessionTranscript
1538
+ ) {
1539
+ try {
1540
+ const transcript = await waitForReplay(
1541
+ this.runtimeFactory.readPersistedSessionTranscript({
1542
+ sessionFile: this.nativeSessionFile,
1543
+ sessionId: this.nativeSessionId,
1544
+ cwd: this.cwd,
1545
+ signal: replay.signal,
1546
+ }),
1547
+ replay.signal,
1548
+ );
1549
+ messages = transcript.messages;
1550
+ } catch (error) {
1551
+ if (replay.signal.aborted) throw error;
1552
+ this.emit({
1553
+ type: "timeline.item",
1554
+ sessionId: this.id,
1555
+ item: {
1556
+ id: "omp:replay-incomplete",
1557
+ type: "error",
1558
+ message:
1559
+ "OMP could not read its complete persisted transcript; displayed history may be incomplete.",
1560
+ },
1561
+ });
1562
+ }
1563
+ }
1564
+ messages ??= await waitForReplay(this.runtime.getMessages(), replay.signal);
1565
+ this.quarantineBranchEntries();
1566
+ for (const message of messages) {
1567
+ replay.signal.throwIfAborted();
1568
+ const entryId = nativeEntryId(message);
1569
+ if (entryId) this.seenEntryIds.add(entryId);
1570
+ this.projector.projectReplayMessage(message);
1571
+ }
1572
+ this.projector.finishReplay();
1573
+ await this.subsessions?.replay(messages, this.runtime, this.runtimeFactory, replay.signal);
1574
+ replay.signal.throwIfAborted();
1575
+ } catch (error) {
1576
+ if (replay.signal.aborted) throw replay.signal.reason;
1577
+ throw error;
1578
+ } finally {
1579
+ clearTimeout(timeoutHandle);
1580
+ this.lifetime.signal.removeEventListener("abort", onAbort);
1581
+ }
1582
+ }
1583
+ async revert(input: SessionRevertInput): Promise<void> {
1584
+ if (input.scope !== "conversation") {
1585
+ this.emit({
1586
+ type: "request.failed",
1587
+ requestId: input.requestId,
1588
+ error: { message: "OMP supports conversation rewind only" },
1589
+ });
1590
+ return;
1591
+ }
1592
+ if (this.activeTurn) {
1593
+ this.emit({
1594
+ type: "request.failed",
1595
+ requestId: input.requestId,
1596
+ error: { message: "Cannot rewind the OMP conversation while a turn is active" },
1597
+ });
1598
+ return;
1599
+ }
1600
+ if (this.revertInFlight || this.configMutationInFlight || this.activeAbort) {
1601
+ this.emit({
1602
+ type: "request.failed",
1603
+ requestId: input.requestId,
1604
+ error: { message: "OMP session is busy" },
1605
+ });
1606
+ return;
1607
+ }
1608
+
1609
+ this.revertInFlight = true;
1610
+ let branchMutationPossible = false;
1611
+ let runtime = this.runtime;
1612
+ let generation = this.generation;
1613
+ try {
1614
+ await this.recoverRuntime();
1615
+ if (this.closed) throw new OmpPublicError("OMP session is closed");
1616
+ if (this.activeTurn) {
1617
+ throw new OmpPublicError("Cannot rewind the OMP conversation while a turn is active");
1618
+ }
1619
+ runtime = this.runtime;
1620
+ generation = this.generation;
1621
+ if (!runtime.canReplayHistory) {
1622
+ throw new OmpPublicError("OMP conversation rewind requires negotiated RPC protocol v2");
1623
+ }
1624
+ const entryId = this.projector.resolveRevertToken(input.token);
1625
+ const [branchMessages, beforeState] = await Promise.all([
1626
+ runtime.getBranchMessages(),
1627
+ runtime.getState(),
1628
+ ]);
1629
+ this.requireCurrentRuntime(runtime, generation);
1630
+ if (this.activeTurn || beforeState.isStreaming || beforeState.isCompacting) {
1631
+ throw new OmpPublicError("Cannot rewind the OMP conversation while a turn is active");
1632
+ }
1633
+ if (!branchMessages.some((message) => message.entryId === entryId)) {
1634
+ throw new OmpPublicError("OMP conversation rewind token is stale");
1635
+ }
1636
+ branchMutationPossible = true;
1637
+ const result = await runtime.branch(entryId);
1638
+ this.requireCurrentRuntime(runtime, generation);
1639
+ if (result.cancelled) {
1640
+ branchMutationPossible = false;
1641
+ throw new OmpPublicError("OMP conversation rewind was cancelled");
1642
+ }
1643
+
1644
+ let state = await runtime.getState();
1645
+ this.requireCurrentRuntime(runtime, generation);
1646
+ const nextNativeSessionId = validateNativeSessionId(state.sessionId);
1647
+ if (nextNativeSessionId !== this.nativeSessionId) {
1648
+ this.transitionNativeSession(this.nativeSessionId, nextNativeSessionId);
1649
+ this.nativeSessionId = nextNativeSessionId;
1650
+ if (this.persistSession) {
1651
+ this.emit({
1652
+ type: "session.persistence",
1653
+ sessionId: this.id,
1654
+ persistence: { version: 1, data: { sessionId: nextNativeSessionId } },
1655
+ });
1656
+ }
1657
+ }
1658
+
1659
+ const modelChanged =
1660
+ beforeState.model?.provider !== state.model?.provider ||
1661
+ beforeState.model?.id !== state.model?.id;
1662
+ const thinkingChanged = beforeState.thinkingLevel !== state.thinkingLevel;
1663
+ if (modelChanged) {
1664
+ if (!beforeState.model) {
1665
+ throw new OmpPublicError("OMP changed model while rewinding the conversation");
1666
+ }
1667
+ await runtime.setModel(beforeState.model.provider, beforeState.model.id);
1668
+ }
1669
+ if (thinkingChanged) {
1670
+ if (!beforeState.thinkingLevel) {
1671
+ throw new OmpPublicError("OMP changed thinking level while rewinding the conversation");
1672
+ }
1673
+ await runtime.setThinkingLevel(beforeState.thinkingLevel);
1674
+ }
1675
+ if (modelChanged || thinkingChanged) {
1676
+ state = await runtime.getState();
1677
+ this.requireCurrentRuntime(runtime, generation);
1678
+ }
1679
+ if (
1680
+ state.sessionId !== this.nativeSessionId ||
1681
+ state.model?.provider !== beforeState.model?.provider ||
1682
+ state.model?.id !== beforeState.model?.id ||
1683
+ state.thinkingLevel !== beforeState.thinkingLevel
1684
+ ) {
1685
+ throw new OmpPublicError("OMP did not preserve session configuration while rewinding");
1686
+ }
1687
+
1688
+ this.projector.resetForRewindReplay();
1689
+ this.quarantineBranchEntries();
1690
+ await this.replayHistory();
1691
+ this.requireCurrentRuntime(runtime, generation);
1692
+ this.publishCommittedConfig(state, runtime, generation);
1693
+ this.emit({ type: "request.completed", requestId: input.requestId });
1694
+ } catch (error) {
1695
+ const failure = branchMutationPossible
1696
+ ? { message: "OMP conversation rewind left native state indeterminate" }
1697
+ : providerError(error, "OMP conversation rewind failed");
1698
+ if (branchMutationPossible) {
1699
+ await this.closeAfterCommittedRewindFailure(runtime, failure.message);
1700
+ }
1701
+ this.emit({ type: "request.failed", requestId: input.requestId, error: failure });
1702
+ if (branchMutationPossible) {
1703
+ this.publishSessionClosed(failure);
1704
+ this.retireRewindSession();
1705
+ }
1706
+ } finally {
1707
+ this.revertInFlight = false;
1708
+ if (this.configRefreshDirty && !this.configMutationInFlight && !this.closed) {
1709
+ this.scheduleCommittedConfigRefresh();
1710
+ }
1711
+ }
1712
+ }
1713
+ private async closeAfterCommittedRewindFailure(
1714
+ runtime: OmpRuntimeSession,
1715
+ message: string,
1716
+ ): Promise<void> {
1717
+ this.closed = true;
1718
+ this.generation += 1;
1719
+ this.runtimeDead = message;
1720
+ this.configRefreshAttempts = 0;
1721
+ this.configRefreshDirty = false;
1722
+ const configRefresh = this.configRefreshInFlight;
1723
+ this.cancelConfigRefreshRetry();
1724
+ this.lifetime.abort(new Error(message));
1725
+ this.resolveAllPermissions(true);
1726
+ this.subsessions?.close();
1727
+ this.projector.close();
1728
+ this.unsubscribe();
1729
+ this.unsubscribe = () => {};
1730
+ this.hostTools.detach();
1731
+ this.runtimeDisposal ??= runtime.close();
1732
+ this.hostToolsDisposal ??= this.hostTools.close();
1733
+ const cleanup = settleSessionCleanup(
1734
+ [
1735
+ this.runtimeDisposal,
1736
+ this.hostToolsDisposal,
1737
+ this.recoveryPromise,
1738
+ configRefresh,
1739
+ this.configRefreshInFlight,
1740
+ ].filter((pending): pending is Promise<void> => pending !== null),
1741
+ );
1742
+ this.disposalPromise = cleanup;
1743
+ this.quarantineRewindCleanup(cleanup);
1744
+ await Promise.allSettled([cleanup]);
1745
+ }
1746
+
1747
+ async prompt(input: SessionPromptInput): Promise<void> {
1748
+ let payload: OmpPromptPayload;
1749
+ try {
1750
+ payload = promptPayload(input);
1751
+ } catch (error) {
1752
+ this.emit({
1753
+ type: "session.prompt_result",
1754
+ sessionId: this.id,
1755
+ clientMessageId: input.prompt.clientMessageId,
1756
+ result: { type: "failed", error: providerError(error, "OMP prompt was rejected") },
1757
+ });
1758
+ return;
1759
+ }
1760
+ if (this.revertInFlight) {
1761
+ this.emit({
1762
+ type: "session.prompt_result",
1763
+ sessionId: this.id,
1764
+ clientMessageId: input.prompt.clientMessageId,
1765
+ result: { type: "failed", error: { message: "OMP conversation rewind is in progress" } },
1766
+ });
1767
+ return;
1768
+ }
1769
+
1770
+ if (input.prompt.delivery === "steer") {
1771
+ await this.steer(input.prompt.clientMessageId, payload);
1772
+ return;
1773
+ }
1774
+ if (
1775
+ payload.commandName &&
1776
+ payload.commandName !== "compact" &&
1777
+ OMP_BUILTIN_COMMANDS.some((command) => command.name === payload.commandName)
1778
+ ) {
1779
+ await this.runBuiltinCommand(input.prompt.clientMessageId, payload);
1780
+ return;
1781
+ }
1782
+ if (this.activeTurn) {
1783
+ await this.routeActivePrompt(
1784
+ input.prompt.clientMessageId,
1785
+ payload,
1786
+ this.activeTurn,
1787
+ input.prompt.delivery === "auto" && input.prompt.input.type === "message",
1788
+ );
1789
+ return;
1790
+ }
1791
+ try {
1792
+ await this.recoverRuntime();
1793
+ } catch (error) {
1794
+ this.emit({
1795
+ type: "session.prompt_result",
1796
+ sessionId: this.id,
1797
+ clientMessageId: input.prompt.clientMessageId,
1798
+ result: { type: "failed", error: providerError(error, "OMP session recovery failed") },
1799
+ });
1800
+ return;
1801
+ }
1802
+ if (this.closed) {
1803
+ this.emit({
1804
+ type: "session.prompt_result",
1805
+ sessionId: this.id,
1806
+ clientMessageId: input.prompt.clientMessageId,
1807
+ result: { type: "failed", error: { message: "OMP session is closed" } },
1808
+ });
1809
+ return;
1810
+ }
1811
+ if (this.revertInFlight) {
1812
+ this.emit({
1813
+ type: "session.prompt_result",
1814
+ sessionId: this.id,
1815
+ clientMessageId: input.prompt.clientMessageId,
1816
+ result: { type: "failed", error: { message: "OMP conversation rewind is in progress" } },
1817
+ });
1818
+ return;
1819
+ }
1820
+ if (this.activeTurn) {
1821
+ await this.routeActivePrompt(
1822
+ input.prompt.clientMessageId,
1823
+ payload,
1824
+ this.activeTurn,
1825
+ input.prompt.delivery === "auto" && input.prompt.input.type === "message",
1826
+ );
1827
+ return;
1828
+ }
1829
+ if (this.activeAbort) {
1830
+ this.emit({
1831
+ type: "session.prompt_result",
1832
+ sessionId: this.id,
1833
+ clientMessageId: input.prompt.clientMessageId,
1834
+ result: { type: "failed", error: { message: "OMP interrupt is still settling" } },
1835
+ });
1836
+ return;
1837
+ }
1838
+
1839
+ if (payload.commandName && !this.slashCommands.has(payload.commandName)) {
1840
+ this.emit({
1841
+ type: "session.prompt_result",
1842
+ sessionId: this.id,
1843
+ clientMessageId: input.prompt.clientMessageId,
1844
+ result: { type: "failed", error: { message: "OMP command is unavailable" } },
1845
+ });
1846
+ return;
1847
+ }
1848
+ let materializedPaths: string[] = [];
1849
+ try {
1850
+ const prepared = this.preparePromptPayload(payload, "prompt");
1851
+ payload = prepared.payload;
1852
+ materializedPaths = prepared.materializedPaths;
1853
+ } catch (error) {
1854
+ this.imageMaterializer.release(materializedPaths);
1855
+ this.emit({
1856
+ type: "session.prompt_result",
1857
+ sessionId: this.id,
1858
+ clientMessageId: input.prompt.clientMessageId,
1859
+ result: { type: "failed", error: providerError(error, "OMP prompt was rejected") },
1860
+ });
1861
+ return;
1862
+ }
1863
+ const turn = createActiveTurn(
1864
+ input.prompt.clientMessageId,
1865
+ payload.text,
1866
+ this.generation,
1867
+ this.runtimeTurnCompleted,
1868
+ slashCommandName(payload.text) === "compact",
1869
+ );
1870
+ this.activeTurn = turn;
1871
+ const runtime = this.runtime;
1872
+ try {
1873
+ if (turn.manualCompaction) {
1874
+ const instructions = payload.text.slice("/compact".length).trim() || undefined;
1875
+ const compaction = runtime.compact(instructions);
1876
+ this.publishPromptResult(turn, { type: "turn", turnId: turn.turnId });
1877
+ turn.starting = false;
1878
+ turn.acknowledged = true;
1879
+ this.startTurn(turn, false);
1880
+ this.startCompaction(turn, "manual");
1881
+ this.pollUsage(turn);
1882
+ const bufferedEvents = turn.bufferedEvents.splice(0);
1883
+ turn.replayingBufferedEvents = true;
1884
+ for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
1885
+ turn.replayingBufferedEvents = false;
1886
+ void this.settleManualCompaction(turn, compaction);
1887
+ turn.manualCompactionDeadlineTimer = this.scheduler.set(() => {
1888
+ turn.manualCompactionDeadlineTimer = undefined;
1889
+ if (
1890
+ turn.terminal ||
1891
+ this.activeTurn !== turn ||
1892
+ turn.generation !== this.generation ||
1893
+ !turn.manualCompactionPending
1894
+ )
1895
+ return;
1896
+ const message = "OMP compaction was canceled after it stopped responding";
1897
+ turn.manualCompactionPending = false;
1898
+ this.invalidateRuntime(message, "canceled");
1899
+ void this.finishTurn(turn, "canceled", undefined, true, true);
1900
+ }, COMPACTION_MAX_WAIT_MS);
1901
+ return;
1902
+ }
1903
+ const ownershipPending = turn.pendingUsers[0];
1904
+ if (turn.terminalOwnershipRequired && !this.branchWatermarkValid && ownershipPending) {
1905
+ await this.refreshBranchEntries(turn, ownershipPending);
1906
+ if (
1907
+ this.closed ||
1908
+ turn.terminal ||
1909
+ this.activeTurn !== turn ||
1910
+ turn.generation !== this.generation
1911
+ ) {
1912
+ return;
1913
+ }
1914
+ }
1915
+ const acknowledgement = await runtime.prompt(
1916
+ payload.text,
1917
+ payload.images,
1918
+ () => {
1919
+ turn.promptAcceptedEventIndex ??= turn.bufferedEvents.length;
1920
+ },
1921
+ (requestId) => {
1922
+ turn.nativeRequestId = requestId;
1923
+ },
1924
+ );
1925
+ if (this.closed || turn.terminal) return;
1926
+ turn.nativeRequestId = acknowledgement.requestId;
1927
+ this.publishPromptResult(turn, { type: "turn", turnId: turn.turnId });
1928
+ this.startTurn(turn);
1929
+ turn.starting = false;
1930
+ turn.acknowledged = true;
1931
+ if (acknowledgement.agentInvoked === true) this.markAgentEvidence(turn);
1932
+ if (acknowledgement.agentInvoked === false && turn.agentInvoked !== true) {
1933
+ turn.agentInvoked = false;
1934
+ turn.localOnlyEligible = true;
1935
+ }
1936
+ const bufferedEvents = turn.bufferedEvents.splice(0);
1937
+ turn.replayingBufferedEvents = true;
1938
+ const preAcceptanceEvents = bufferedEvents.splice(
1939
+ 0,
1940
+ turn.promptAcceptedEventIndex ?? bufferedEvents.length,
1941
+ );
1942
+ for (const event of preAcceptanceEvents) this.handleTurnEvent(turn, event);
1943
+ this.projector.acceptLiveTurn(turn.turnId);
1944
+ for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
1945
+ turn.replayingBufferedEvents = false;
1946
+ if (acknowledgement.agentInvoked === true && turn.bufferedTerminalOwnershipEvidence) {
1947
+ this.markTerminalOwnershipEvidence(turn);
1948
+ }
1949
+ if (
1950
+ acknowledgement.agentInvoked === false &&
1951
+ turn.agentInvoked !== true &&
1952
+ !turn.nativeActivity &&
1953
+ !turn.awaitingPermissionEvidence
1954
+ ) {
1955
+ this.scheduleLocalOnlyCompletion(turn);
1956
+ }
1957
+ } catch (error) {
1958
+ this.imageMaterializer.release(materializedPaths);
1959
+ const failure = providerError(error, "OMP prompt failed");
1960
+ if (this.isCurrentRuntime(runtime, turn.generation)) {
1961
+ this.handleRuntimeFailure(failure.message);
1962
+ return;
1963
+ }
1964
+ this.publishPendingUsers(turn);
1965
+ this.publishPromptResult(turn, { type: "failed", error: failure });
1966
+ this.subsessions?.terminalize("failed");
1967
+ if (turn.started) await this.finishTurn(turn, "failed", failure);
1968
+ else {
1969
+ turn.steerReady.resolve();
1970
+ turn.terminal = true;
1971
+ this.finishCompaction("failed", { message: "OMP prompt failed" });
1972
+ this.resolveTurnPermissions(turn.turnId);
1973
+ this.projector.finishTurn(turn.turnId);
1974
+ if (this.activeTurn === turn) this.activeTurn = null;
1975
+ }
1976
+ }
1977
+ }
1978
+ private preparePromptPayload(
1979
+ payload: OmpPromptPayload,
1980
+ delivery: "prompt" | "steer",
1981
+ ): {
1982
+ payload: OmpPromptPayload;
1983
+ materializedPaths: string[];
1984
+ } {
1985
+ const currentModel = this.configState.model
1986
+ ? this.nativeModelsByPublicId.get(this.configState.model)
1987
+ : undefined;
1988
+ // OMP chunks protocol v2 output only; every stdin command must fit one physical frame.
1989
+ const inlineImagesFit =
1990
+ this.runtime.maxInputFrameBytes === undefined ||
1991
+ inlinePromptFrameBytes(payload, delivery) <= this.runtime.maxInputFrameBytes;
1992
+ if (
1993
+ payload.images.length === 0 ||
1994
+ (currentModel?.input?.includes("image") && inlineImagesFit)
1995
+ ) {
1996
+ return { payload, materializedPaths: [] };
1997
+ }
1998
+ const materializedPaths: string[] = [];
1999
+ try {
2000
+ for (const image of payload.images) {
2001
+ if (!isOmpImageMimeType(image.mimeType)) {
2002
+ throw new OmpPublicError("OMP prompt image is invalid");
2003
+ }
2004
+ materializedPaths.push(this.imageMaterializer.materialize(image.data, image.mimeType));
2005
+ }
2006
+ const hints = materializedPaths.map((path) => `[Image available at: ${path}]`);
2007
+ const text = [payload.text, ...hints].filter(Boolean).join("\n\n");
2008
+ if (utf8Bytes(text) > MAX_PROMPT_TEXT_LENGTH) {
2009
+ throw new OmpPublicError("OMP prompt is too large");
2010
+ }
2011
+ return { payload: { ...payload, text, images: [] }, materializedPaths };
2012
+ } catch (error) {
2013
+ this.imageMaterializer.release(materializedPaths);
2014
+ throw error;
2015
+ }
2016
+ }
2017
+
2018
+ private async runBuiltinCommand(
2019
+ clientMessageId: string,
2020
+ payload: OmpPromptPayload,
2021
+ ): Promise<void> {
2022
+ const commandName = payload.commandName;
2023
+ if (!commandName) return;
2024
+ const argumentsText = payload.text.slice(commandName.length + 1).trim();
2025
+ try {
2026
+ if (commandName === "steer") {
2027
+ if (!argumentsText) throw new OmpPublicError("Usage: /steer <message>");
2028
+ await this.steer(clientMessageId, { text: argumentsText, images: [] });
2029
+ return;
2030
+ }
2031
+ if (this.activeTurn && (commandName === "handoff" || commandName === "follow-up")) {
2032
+ throw new OmpPublicError("OMP already has an active turn; send this message as a steer");
2033
+ }
2034
+ await this.recoverRuntime();
2035
+ if (this.closed) throw new OmpPublicError("OMP session is closed");
2036
+ if (commandName === "follow-up") {
2037
+ if (!argumentsText) throw new OmpPublicError("Usage: /follow-up <message>");
2038
+ await this.startNativeCommandTurn(clientMessageId, argumentsText, () =>
2039
+ this.runtime.followUp(argumentsText),
2040
+ );
2041
+ return;
2042
+ }
2043
+ if (commandName === "handoff") {
2044
+ await this.startNativeCommandTurn(clientMessageId, payload.text, () =>
2045
+ this.runtime.handoff(argumentsText || undefined),
2046
+ );
2047
+ return;
2048
+ }
2049
+ if (commandName === "autocompact") {
2050
+ const requested = argumentsText.toLowerCase() || "toggle";
2051
+ if (!["on", "off", "toggle"].includes(requested)) {
2052
+ throw new OmpPublicError("Usage: /autocompact [on|off|toggle]");
2053
+ }
2054
+ let enabled = requested === "on";
2055
+ if (requested === "toggle") {
2056
+ const state = await this.runtime.getState();
2057
+ if (typeof state.autoCompactionEnabled !== "boolean") {
2058
+ throw new OmpPublicError(
2059
+ "Auto-compaction state is unavailable. Use /autocompact on or /autocompact off.",
2060
+ );
2061
+ }
2062
+ enabled = !state.autoCompactionEnabled;
2063
+ }
2064
+ await this.runtime.setAutoCompaction(enabled);
2065
+ this.emit({
2066
+ type: "timeline.item",
2067
+ sessionId: this.id,
2068
+ item: {
2069
+ id: `omp:command:${randomUUID()}`,
2070
+ type: "assistant_message",
2071
+ text: `Auto-compaction ${enabled ? "enabled" : "disabled"}.`,
2072
+ },
2073
+ });
2074
+ }
2075
+ this.emit({
2076
+ type: "session.prompt_result",
2077
+ sessionId: this.id,
2078
+ clientMessageId,
2079
+ result: { type: "completed" },
2080
+ });
2081
+ } catch (error) {
2082
+ this.emit({
2083
+ type: "session.prompt_result",
2084
+ sessionId: this.id,
2085
+ clientMessageId,
2086
+ result: { type: "failed", error: providerError(error, "OMP command failed") },
2087
+ });
2088
+ }
2089
+ }
2090
+
2091
+ private async startNativeCommandTurn(
2092
+ clientMessageId: string,
2093
+ text: string,
2094
+ invoke: () => Promise<void>,
2095
+ ): Promise<void> {
2096
+ if (this.activeTurn) {
2097
+ throw new OmpPublicError("OMP already has an active turn; send this message as a steer");
2098
+ }
2099
+ const turn = createActiveTurn(clientMessageId, text, this.generation, false);
2100
+ this.activeTurn = turn;
2101
+ try {
2102
+ await invoke();
2103
+ if (this.closed || turn.terminal || this.activeTurn !== turn) {
2104
+ if (!turn.terminal) {
2105
+ this.publishPendingUsers(turn);
2106
+ this.publishPromptResult(turn, {
2107
+ type: "failed",
2108
+ error: {
2109
+ message: this.closed ? "OMP session is closed" : "OMP command lost turn ownership",
2110
+ },
2111
+ });
2112
+ this.settleUnstartedTurn(turn);
2113
+ }
2114
+ return;
2115
+ }
2116
+ this.publishPromptResult(turn, { type: "turn", turnId: turn.turnId });
2117
+ turn.starting = false;
2118
+ turn.acknowledged = true;
2119
+ this.startTurn(turn);
2120
+ const bufferedEvents = turn.bufferedEvents.splice(0);
2121
+ turn.replayingBufferedEvents = true;
2122
+ this.projector.acceptLiveTurn(turn.turnId);
2123
+ for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
2124
+ turn.replayingBufferedEvents = false;
2125
+ } catch (error) {
2126
+ if (turn.terminal) return;
2127
+ const ownsTurn = this.activeTurn === turn;
2128
+ const failure = providerError(error, "OMP command failed");
2129
+ this.publishPendingUsers(turn);
2130
+ this.publishPromptResult(turn, { type: "failed", error: failure });
2131
+ this.settleUnstartedTurn(turn);
2132
+ if (ownsTurn) this.imageMaterializer.clear();
2133
+ }
2134
+ }
2135
+
2136
+ private async settleManualCompaction(
2137
+ turn: ActiveTurn,
2138
+ compaction: Promise<OmpCompactionResult>,
2139
+ ): Promise<void> {
2140
+ try {
2141
+ const result = await compaction;
2142
+ turn.manualCompactionPending = false;
2143
+ if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
2144
+ this.finishCompaction("completed", { tokensBefore: result.tokensBefore });
2145
+ await this.finishTurn(turn, turn.interrupted ? "canceled" : "completed");
2146
+ } catch (error) {
2147
+ if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
2148
+ turn.manualCompactionPending = false;
2149
+ const raw = providerError(error, "OMP compaction failed");
2150
+ const failure = { message: this.dataFilter.text(raw.message, 4_096) };
2151
+ const state = turn.interrupted ? "canceled" : "failed";
2152
+ this.finishCompaction(state, { message: failure.message });
2153
+ await this.finishTurn(turn, state, state === "failed" ? failure : undefined, true, true);
2154
+ }
2155
+ }
2156
+
2157
+ async interrupt(input: SessionInterruptInput): Promise<void> {
2158
+ const turn = this.activeTurn;
2159
+ if (!turn) {
2160
+ this.emit({ type: "request.completed", requestId: input.requestId });
2161
+ return;
2162
+ }
2163
+ if (turn.manualCompaction) {
2164
+ turn.interrupted = true;
2165
+ turn.manualCompactionPending = false;
2166
+ this.invalidateRuntime("OMP compaction interrupted", "canceled");
2167
+ await this.finishTurn(turn, "canceled", undefined, true, true);
2168
+ this.emit({ type: "request.completed", requestId: input.requestId });
2169
+ return;
2170
+ }
2171
+ const forceTerminal = turn.awaitingPermissionEvidence;
2172
+ turn.awaitingPermissionEvidence = false;
2173
+ if (forceTerminal) this.resolveTurnPermissions(turn.turnId);
2174
+ const pending = this.activeAbort;
2175
+ if (turn.interrupted) {
2176
+ if (pending?.turn === turn) await this.settleInterrupt(input.requestId, pending);
2177
+ else this.emit({ type: "request.completed", requestId: input.requestId });
2178
+ return;
2179
+ }
2180
+ turn.interrupted = true;
2181
+ const runtime = this.runtime;
2182
+ const abort: PendingAbort = {
2183
+ turn,
2184
+ generation: turn.generation,
2185
+ runtime,
2186
+ forceTerminal,
2187
+ promise: runtime.abort(),
2188
+ };
2189
+ this.activeAbort = abort;
2190
+ await this.settleInterrupt(input.requestId, abort);
2191
+ }
2192
+
2193
+ private async settleInterrupt(requestId: string, abort: PendingAbort): Promise<void> {
2194
+ try {
2195
+ await abort.promise;
2196
+ if (
2197
+ (abort.turn.terminalizing || abort.forceTerminal) &&
2198
+ !abort.turn.terminal &&
2199
+ this.activeTurn === abort.turn
2200
+ ) {
2201
+ await this.finishTurn(abort.turn, "canceled", undefined, true, true);
2202
+ }
2203
+ this.emit({ type: "request.completed", requestId });
2204
+ } catch (error) {
2205
+ const { runtime, turn } = abort;
2206
+ if (
2207
+ this.runtimeDead ||
2208
+ turn.terminal ||
2209
+ turn.generation !== this.generation ||
2210
+ this.runtime !== runtime
2211
+ ) {
2212
+ await this.runtimeDisposal?.catch(() => undefined);
2213
+ this.emit({ type: "request.completed", requestId });
2214
+ return;
2215
+ }
2216
+ turn.interrupted = false;
2217
+ this.emit({
2218
+ type: "request.failed",
2219
+ requestId,
2220
+ error: providerError(error, "OMP interrupt failed"),
2221
+ });
2222
+ } finally {
2223
+ if (this.activeAbort === abort) this.activeAbort = null;
2224
+ }
2225
+ }
2226
+
2227
+ beginConnectionShutdown(): void {
2228
+ if (this.closed) return;
2229
+ this.closed = true;
2230
+ this.lifetime.abort(new Error("OMP provider connection closed"));
2231
+ this.unsubscribe();
2232
+ this.unsubscribe = () => {};
2233
+ const turn = this.activeTurn;
2234
+ if (turn && !turn.started) {
2235
+ this.publishPromptResult(turn, {
2236
+ type: "failed",
2237
+ error: { message: "OMP session closed before the prompt was accepted" },
2238
+ });
2239
+ this.settleUnstartedTurn(turn);
2240
+ }
2241
+ }
2242
+ async configure(input: SessionConfigureInput): Promise<void> {
2243
+ if (this.revertInFlight) {
2244
+ this.emit({
2245
+ type: "request.failed",
2246
+ requestId: input.requestId,
2247
+ error: { message: "OMP conversation rewind is in progress" },
2248
+ });
2249
+ return;
2250
+ }
2251
+ if (this.configMutationInFlight) {
2252
+ this.emit({
2253
+ type: "request.failed",
2254
+ requestId: input.requestId,
2255
+ error: { message: "OMP configuration is already in progress" },
2256
+ });
2257
+ return;
2258
+ }
2259
+ const runtime = this.runtime;
2260
+ const generation = this.generation;
2261
+ this.configMutationInFlight = true;
2262
+ let mutationAttempted = false;
2263
+ try {
2264
+ if (!this.isCurrentRuntime(runtime, generation)) {
2265
+ throw new OmpPublicError("OMP session is unavailable for configuration");
2266
+ }
2267
+ if (input.changes.mode !== undefined && input.changes.mode !== this.configState.mode) {
2268
+ throw new OmpPublicError(
2269
+ "OMP approval mode cannot change live; create a new session instead",
2270
+ );
2271
+ }
2272
+ if (input.changes.settings && Object.keys(input.changes.settings).length > 0) {
2273
+ throw new OmpPublicError("OMP does not expose live provider settings");
2274
+ }
2275
+ if (input.changes.model === null || input.changes.thinkingOption === null) {
2276
+ throw new OmpPublicError("OMP model and thinking selections cannot be cleared");
2277
+ }
2278
+ const targetModelId = input.changes.model ?? this.configState.model;
2279
+ const targetModel = targetModelId
2280
+ ? this.nativeModelsByPublicId.get(targetModelId)
2281
+ : undefined;
2282
+ if (input.changes.model !== undefined && !targetModel) {
2283
+ throw new OmpPublicError("OMP model selection is unavailable");
2284
+ }
2285
+ if (
2286
+ input.changes.thinkingOption !== undefined &&
2287
+ !thinkingForModel(targetModel).some((option) => option.id === input.changes.thinkingOption)
2288
+ ) {
2289
+ throw new OmpPublicError("OMP thinking level is unavailable for the selected model");
2290
+ }
2291
+ if (input.changes.model !== undefined || input.changes.thinkingOption !== undefined) {
2292
+ this.configRevision += 1;
2293
+ }
2294
+ if (input.changes.model && targetModel) {
2295
+ mutationAttempted = true;
2296
+ await runtime.setModel(targetModel.provider, targetModel.id);
2297
+ this.requireCurrentRuntime(runtime, generation);
2298
+ }
2299
+ if (input.changes.thinkingOption) {
2300
+ mutationAttempted = true;
2301
+ await runtime.setThinkingLevel(input.changes.thinkingOption);
2302
+ this.requireCurrentRuntime(runtime, generation);
2303
+ }
2304
+ const state = await runtime.getState();
2305
+ this.requireCurrentRuntime(runtime, generation);
2306
+ if (!this.publishCommittedConfig(state, runtime, generation)) {
2307
+ throw new OmpPublicError("OMP session changed before configuration committed");
2308
+ }
2309
+ const committedModel = state.model ? ompModelId(state.model) : undefined;
2310
+ if (input.changes.model !== undefined && input.changes.model !== committedModel) {
2311
+ throw new OmpPublicError("OMP did not commit the requested model");
2312
+ }
2313
+ if (
2314
+ input.changes.thinkingOption !== undefined &&
2315
+ input.changes.thinkingOption !== state.thinkingLevel
2316
+ ) {
2317
+ throw new OmpPublicError("OMP did not commit the requested thinking level");
2318
+ }
2319
+ this.requireCurrentRuntime(runtime, generation);
2320
+ this.emit({ type: "request.completed", requestId: input.requestId });
2321
+ } catch (error) {
2322
+ if (error instanceof OmpCatalogEscape && this.isCurrentRuntime(runtime, generation)) {
2323
+ this.handleRuntimeFailure(error.message);
2324
+ } else if (mutationAttempted && this.isCurrentRuntime(runtime, generation)) {
2325
+ const state = await this.readRuntimeStateWithTimeout(runtime);
2326
+ if (state && this.isCurrentRuntime(runtime, generation)) {
2327
+ try {
2328
+ this.publishCommittedConfig(state, runtime, generation);
2329
+ } catch (refreshError) {
2330
+ if (
2331
+ refreshError instanceof OmpCatalogEscape &&
2332
+ this.isCurrentRuntime(runtime, generation)
2333
+ ) {
2334
+ this.handleRuntimeFailure(refreshError.message);
2335
+ }
2336
+ }
2337
+ }
2338
+ }
2339
+ this.emit({
2340
+ type: "request.failed",
2341
+ requestId: input.requestId,
2342
+ error: providerError(error, "OMP configuration failed"),
2343
+ });
2344
+ } finally {
2345
+ this.configMutationInFlight = false;
2346
+ if (this.configRefreshDirty && this.isCurrentRuntime(runtime, generation)) {
2347
+ this.scheduleCommittedConfigRefresh();
2348
+ }
2349
+ }
2350
+ }
2351
+
2352
+ private isCurrentRuntime(runtime: OmpRuntimeSession, generation: number): boolean {
2353
+ return (
2354
+ !this.closed &&
2355
+ this.runtimeDead === null &&
2356
+ this.runtime === runtime &&
2357
+ this.generation === generation
2358
+ );
2359
+ }
2360
+
2361
+ private requireCurrentRuntime(runtime: OmpRuntimeSession, generation: number): void {
2362
+ if (!this.isCurrentRuntime(runtime, generation)) {
2363
+ throw new OmpPublicError("OMP session changed while configuration was pending");
2364
+ }
2365
+ }
2366
+
2367
+ private scheduleCommittedConfigRefresh(): void {
2368
+ this.configRefreshDirty = true;
2369
+ if (!this.readyPublished) return;
2370
+ if (
2371
+ this.configRefreshInFlight ||
2372
+ this.configRefreshRetryResolve ||
2373
+ this.configMutationInFlight ||
2374
+ this.revertInFlight
2375
+ ) {
2376
+ return;
2377
+ }
2378
+ const runtime = this.runtime;
2379
+ const generation = this.generation;
2380
+ const refresh = this.refreshCommittedConfig(runtime, generation);
2381
+ this.configRefreshInFlight = refresh;
2382
+ const settleRefresh = (failed: boolean) => {
2383
+ if (this.configRefreshInFlight !== refresh) return;
2384
+ this.configRefreshInFlight = null;
2385
+ try {
2386
+ if (failed) this.scheduleConfigRefreshRetry(runtime, generation);
2387
+ else if (this.configRefreshDirty && this.isCurrentRuntime(runtime, generation)) {
2388
+ this.scheduleCommittedConfigRefresh();
2389
+ }
2390
+ } catch {
2391
+ this.configRefreshDirty = false;
2392
+ if (this.isCurrentRuntime(runtime, generation)) {
2393
+ try {
2394
+ this.handleRuntimeFailure("OMP runtime configuration refresh failed");
2395
+ } catch {
2396
+ // The runtime was already invalidated; detached refresh failures are contained.
2397
+ }
2398
+ }
2399
+ }
2400
+ };
2401
+ void refresh.then(
2402
+ () => settleRefresh(false),
2403
+ () => settleRefresh(true),
2404
+ );
2405
+ }
2406
+
2407
+ private async refreshCommittedConfig(
2408
+ runtime: OmpRuntimeSession,
2409
+ generation: number,
2410
+ ): Promise<void> {
2411
+ this.configRefreshDirty = false;
2412
+ const revision = this.configRevision;
2413
+ const state = await this.readRuntimeStateWithTimeout(runtime);
2414
+ if (!this.isCurrentRuntime(runtime, generation)) return;
2415
+ if (revision !== this.configRevision) {
2416
+ this.configRefreshDirty = true;
2417
+ return;
2418
+ }
2419
+ if (!state) {
2420
+ this.scheduleConfigRefreshRetry(runtime, generation);
2421
+ return;
2422
+ }
2423
+ try {
2424
+ this.publishCommittedConfig(state, runtime, generation);
2425
+ } catch (error) {
2426
+ if (error instanceof OmpCatalogEscape) {
2427
+ this.handleRuntimeFailure(error.message);
2428
+ return;
2429
+ }
2430
+ this.scheduleConfigRefreshRetry(runtime, generation);
2431
+ }
2432
+ }
2433
+
2434
+ private scheduleConfigRefreshRetry(runtime: OmpRuntimeSession, generation: number): void {
2435
+ if (!this.isCurrentRuntime(runtime, generation)) return;
2436
+ this.configRefreshDirty = true;
2437
+ this.configRefreshAttempts += 1;
2438
+ if (this.configRefreshAttempts >= CONFIG_REFRESH_MAX_ATTEMPTS) {
2439
+ this.configRefreshDirty = false;
2440
+ this.handleRuntimeFailure("OMP runtime configuration state remained unavailable");
2441
+ return;
2442
+ }
2443
+ if (this.configRefreshRetryResolve) return;
2444
+ const retry = Promise.withResolvers<void>();
2445
+ const delayMs = CONFIG_REFRESH_RETRY_BASE_MS * 2 ** (this.configRefreshAttempts - 1);
2446
+ const timer = this.scheduler.set(retry.resolve, delayMs);
2447
+ this.configRefreshRetryHandle = timer;
2448
+ this.configRefreshRetryResolve = retry.resolve;
2449
+ const finishRetry = () => {
2450
+ if (this.configRefreshRetryResolve !== retry.resolve) return;
2451
+ this.configRefreshRetryHandle = null;
2452
+ this.configRefreshRetryResolve = null;
2453
+ try {
2454
+ this.scheduler.clear(timer);
2455
+ } catch {
2456
+ // The one-shot callback already fired; a cleanup failure must not wedge refreshes.
2457
+ }
2458
+ if (this.configRefreshDirty && this.isCurrentRuntime(runtime, generation)) {
2459
+ this.scheduleCommittedConfigRefresh();
2460
+ }
2461
+ };
2462
+ void retry.promise.then(finishRetry, finishRetry);
2463
+ }
2464
+
2465
+ private cancelConfigRefreshRetry(): void {
2466
+ const resolve = this.configRefreshRetryResolve;
2467
+ if (!resolve) return;
2468
+ const handle = this.configRefreshRetryHandle;
2469
+ this.configRefreshRetryHandle = null;
2470
+ this.configRefreshRetryResolve = null;
2471
+ if (handle !== null) {
2472
+ try {
2473
+ this.scheduler.clear(handle);
2474
+ } catch {
2475
+ // Resolving below is authoritative even when scheduler cleanup reports failure.
2476
+ }
2477
+ }
2478
+ resolve();
2479
+ }
2480
+
2481
+ private async readRuntimeStateWithTimeout(
2482
+ runtime: OmpRuntimeSession,
2483
+ ): Promise<OmpSessionState | undefined> {
2484
+ const stateRequest = runtime.getState();
2485
+ void stateRequest.catch(() => undefined);
2486
+ const timeout = Promise.withResolvers<null>();
2487
+ const timer = this.scheduler.set(() => timeout.resolve(null), AGENT_END_STATE_TIMEOUT_MS);
2488
+ try {
2489
+ return (await Promise.race([stateRequest, timeout.promise])) ?? undefined;
2490
+ } catch {
2491
+ return undefined;
2492
+ } finally {
2493
+ this.scheduler.clear(timer);
2494
+ }
2495
+ }
2496
+
2497
+ private async readRuntimeHistoryWithTimeout(
2498
+ runtime: OmpRuntimeSession,
2499
+ ): Promise<OmpMessage[] | undefined> {
2500
+ if (!runtime.canReplayHistory) return undefined;
2501
+ const historyRequest = runtime.getMessages();
2502
+ void historyRequest.catch(() => undefined);
2503
+ const timeout = Promise.withResolvers<null>();
2504
+ const timer = this.scheduler.set(() => timeout.resolve(null), AGENT_END_HISTORY_TIMEOUT_MS);
2505
+ try {
2506
+ return (await Promise.race([historyRequest, timeout.promise])) ?? undefined;
2507
+ } catch {
2508
+ return undefined;
2509
+ } finally {
2510
+ this.scheduler.clear(timer);
2511
+ }
2512
+ }
2513
+
2514
+ private publishCommittedConfig(
2515
+ state: OmpSessionState,
2516
+ runtime: OmpRuntimeSession,
2517
+ generation: number,
2518
+ force = false,
2519
+ ): boolean {
2520
+ if (!this.isCurrentRuntime(runtime, generation)) return false;
2521
+ const publicModelId = state.model ? ompModelId(state.model) : undefined;
2522
+ const advertisedModel = publicModelId
2523
+ ? this.nativeModelsByPublicId.get(publicModelId)
2524
+ : undefined;
2525
+ if (state.model && !advertisedModel) {
2526
+ throw new OmpCatalogEscape("OMP runtime selected an unadvertised model");
2527
+ }
2528
+ const thinkingOptions = thinkingForModel(advertisedModel);
2529
+ const committedThinkingLevel = applicableThinkingLevel(advertisedModel, state.thinkingLevel);
2530
+ if (
2531
+ committedThinkingLevel &&
2532
+ !thinkingOptions.some((option) => option.id === committedThinkingLevel)
2533
+ ) {
2534
+ throw new OmpCatalogEscape("OMP runtime selected an unsupported thinking level");
2535
+ }
2536
+ this.configRefreshAttempts = 0;
2537
+ this.cancelConfigRefreshRetry();
2538
+ const nextConfig: ProviderConfigState = {
2539
+ ...this.configState,
2540
+ ...(publicModelId ? { model: publicModelId } : { model: undefined }),
2541
+ ...(committedThinkingLevel
2542
+ ? { thinkingOption: committedThinkingLevel }
2543
+ : { thinkingOption: undefined }),
2544
+ thinkingOptions,
2545
+ };
2546
+ const changed =
2547
+ force ||
2548
+ nextConfig.model !== this.configState.model ||
2549
+ nextConfig.thinkingOption !== this.configState.thinkingOption ||
2550
+ nextConfig.thinkingOptions.length !== this.configState.thinkingOptions.length ||
2551
+ nextConfig.thinkingOptions.some(
2552
+ (option, index) =>
2553
+ option.id !== this.configState.thinkingOptions[index]?.id ||
2554
+ option.isDefault !== this.configState.thinkingOptions[index]?.isDefault,
2555
+ );
2556
+ this.configState = nextConfig;
2557
+ this.recoveryOptions = withCommittedOmpSelection(this.recoveryOptions, {
2558
+ model: state.model ? nativeOmpModelId(state.model) : undefined,
2559
+ thinkingOption: this.configState.thinkingOption,
2560
+ });
2561
+ if (changed) {
2562
+ this.emit({ type: "session.config", sessionId: this.id, config: this.configState });
2563
+ }
2564
+ return true;
2565
+ }
2566
+
2567
+ abortOpen(): Promise<void> {
2568
+ this.disposalPromise ??= this.disposeSession();
2569
+ return this.disposalPromise;
2570
+ }
2571
+
2572
+ async permission(input: SessionPermissionInput): Promise<void> {
2573
+ const typed = this.pendingToolPermissions.get(input.permissionId);
2574
+ if (typed) {
2575
+ await this.respondToToolPermission(input, typed);
2576
+ return;
2577
+ }
2578
+ const pending = this.pendingPermissions.get(input.permissionId);
2579
+ if (!pending) throw new OmpPublicError("Unknown OMP permission request");
2580
+ if (!this.permissionOwnerIsCurrent(pending)) {
2581
+ this.pendingPermissions.delete(input.permissionId);
2582
+ if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
2583
+ this.emit({
2584
+ type: "session.permission_resolved",
2585
+ sessionId: this.id,
2586
+ permissionId: input.permissionId,
2587
+ });
2588
+ throw new OmpPublicError("OMP permission request is no longer active");
2589
+ }
2590
+ const { generation, runtime } = pending;
2591
+ this.pendingPermissions.delete(input.permissionId);
2592
+ this.inFlightPermissions.set(input.permissionId, pending);
2593
+ if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
2594
+ try {
2595
+ const freeformValue = this.freeformSelection(pending, input.response);
2596
+ if (freeformValue !== undefined) {
2597
+ this.pendingFreeformSelection = {
2598
+ value: freeformValue,
2599
+ nativeSelectId: pending.nativeId,
2600
+ generation,
2601
+ runtime,
2602
+ ...(pending.turnId ? { turnId: pending.turnId } : {}),
2603
+ };
2604
+ }
2605
+ const response = this.extensionUiResponse(pending, input.response);
2606
+ await runtime.respondToExtensionUi(response);
2607
+ } catch (error) {
2608
+ if (this.pendingFreeformSelection?.nativeSelectId === pending.nativeId) {
2609
+ this.pendingFreeformSelection = null;
2610
+ }
2611
+ if (this.inFlightPermissions.get(input.permissionId) !== pending) return;
2612
+ this.inFlightPermissions.delete(input.permissionId);
2613
+ if (
2614
+ this.permissionOwnerIsCurrent(pending) &&
2615
+ generation === this.generation &&
2616
+ runtime === this.runtime
2617
+ ) {
2618
+ this.pendingPermissions.set(input.permissionId, pending);
2619
+ this.armPermissionTimeout(input.permissionId, pending);
2620
+ throw error;
2621
+ }
2622
+ return;
2623
+ }
2624
+ if (this.inFlightPermissions.get(input.permissionId) !== pending) return;
2625
+ this.inFlightPermissions.delete(input.permissionId);
2626
+ this.emit({
2627
+ type: "session.permission_resolved",
2628
+ sessionId: this.id,
2629
+ permissionId: input.permissionId,
2630
+ });
2631
+ this.reevaluateDeferredPermissionTerminal();
2632
+ }
2633
+ private async respondToToolPermission(
2634
+ input: SessionPermissionInput,
2635
+ pending: PendingToolPermission,
2636
+ ): Promise<void> {
2637
+ if (!this.permissionOwnerIsCurrent(pending)) {
2638
+ this.pendingToolPermissions.delete(input.permissionId);
2639
+ if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
2640
+ this.resolvedToolApprovalIds.add(pending.nativeId);
2641
+ this.emit({
2642
+ type: "session.permission_resolved",
2643
+ sessionId: this.id,
2644
+ permissionId: input.permissionId,
2645
+ });
2646
+ throw new OmpPublicError("OMP permission request is no longer active");
2647
+ }
2648
+ if (
2649
+ input.response.selectedActionId !== undefined &&
2650
+ input.response.selectedActionId !== input.response.behavior
2651
+ ) {
2652
+ throw new OmpPublicError("OMP permission action is invalid");
2653
+ }
2654
+ this.pendingToolPermissions.delete(input.permissionId);
2655
+ this.inFlightToolPermissions.set(input.permissionId, pending);
2656
+ if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
2657
+ try {
2658
+ await pending.runtime.respondToToolApproval({
2659
+ type: "tool_approval_response",
2660
+ id: pending.nativeId,
2661
+ toolCallId: pending.toolCallId,
2662
+ approved: input.response.behavior === "allow",
2663
+ });
2664
+ } catch (error) {
2665
+ if (this.inFlightToolPermissions.get(input.permissionId) !== pending) return;
2666
+ this.inFlightToolPermissions.delete(input.permissionId);
2667
+ this.resolvedToolApprovalIds.add(pending.nativeId);
2668
+ this.emit({
2669
+ type: "session.permission_resolved",
2670
+ sessionId: this.id,
2671
+ permissionId: input.permissionId,
2672
+ });
2673
+ this.reevaluateDeferredPermissionTerminal();
2674
+ throw error;
2675
+ }
2676
+ if (this.inFlightToolPermissions.get(input.permissionId) !== pending) return;
2677
+ this.inFlightToolPermissions.delete(input.permissionId);
2678
+ this.resolvedToolApprovalIds.add(pending.nativeId);
2679
+ this.emit({
2680
+ type: "session.permission_resolved",
2681
+ sessionId: this.id,
2682
+ permissionId: input.permissionId,
2683
+ });
2684
+ this.reevaluateDeferredPermissionTerminal();
2685
+ }
2686
+
2687
+ close(input?: SessionCloseInput): Promise<void> {
2688
+ this.disposalPromise ??= this.disposeSession();
2689
+ return this.disposalPromise.then(
2690
+ () => {
2691
+ this.publishSessionClosed();
2692
+ if (input) this.emit({ type: "request.completed", requestId: input.requestId });
2693
+ },
2694
+ (error) => {
2695
+ const failure = providerError(error, "OMP session close failed");
2696
+ this.publishSessionClosed(failure);
2697
+ if (input) {
2698
+ this.emit({ type: "request.failed", requestId: input.requestId, error: failure });
2699
+ return;
2700
+ }
2701
+ throw error;
2702
+ },
2703
+ );
2704
+ }
2705
+
2706
+ private async disposeSession(): Promise<void> {
2707
+ this.closed = true;
2708
+ this.lifetime.abort(new Error("OMP provider session closed"));
2709
+ const turn = this.activeTurn;
2710
+ if (turn) {
2711
+ this.publishPromptResult(turn, {
2712
+ type: "failed",
2713
+ error: { message: "OMP session closed before the prompt was accepted" },
2714
+ });
2715
+ if (turn.started) await this.finishTurn(turn, "canceled", undefined, true, true);
2716
+ else {
2717
+ this.settleUnstartedTurn(turn);
2718
+ this.finishCompaction("canceled");
2719
+ }
2720
+ }
2721
+ this.imageMaterializer.clear();
2722
+ this.finishCompaction("canceled");
2723
+ this.resolveAllPermissions(true);
2724
+ this.closed = true;
2725
+ this.configRefreshAttempts = 0;
2726
+ this.configRefreshDirty = false;
2727
+ const configRefresh = this.configRefreshInFlight;
2728
+ this.cancelConfigRefreshRetry();
2729
+ this.lifetime.abort(new Error("OMP provider session closed"));
2730
+ this.subsessions?.close();
2731
+ this.projector.close();
2732
+ this.unsubscribe();
2733
+ this.hostTools.detach();
2734
+ this.runtimeDisposal ??= this.runtime.close();
2735
+ this.hostToolsDisposal ??= this.hostTools.close();
2736
+ const cleanupErrors: unknown[] = [];
2737
+ const deferredCleanup: Promise<void>[] = [];
2738
+ const seenCleanup = new Set<Promise<void>>();
2739
+ const seenCoordination = new Set<Promise<void>>();
2740
+ while (true) {
2741
+ const cleanup = [this.runtimeDisposal, this.hostToolsDisposal].filter(
2742
+ (promise): promise is Promise<void> => promise !== null && !seenCleanup.has(promise),
2743
+ );
2744
+ const coordination = [this.recoveryPromise, configRefresh, this.configRefreshInFlight].filter(
2745
+ (promise): promise is Promise<void> => promise !== null && !seenCoordination.has(promise),
2746
+ );
2747
+ if (cleanup.length === 0 && coordination.length === 0) break;
2748
+ for (const promise of cleanup) seenCleanup.add(promise);
2749
+ for (const promise of coordination) seenCoordination.add(promise);
2750
+ const results = await Promise.allSettled([...cleanup, ...coordination]);
2751
+ for (const result of results.slice(0, cleanup.length)) {
2752
+ if (result.status !== "rejected") continue;
2753
+ if (isOmpCleanupFailure(result.reason)) {
2754
+ if (!seenCleanup.has(result.reason.cleanup)) deferredCleanup.push(result.reason.cleanup);
2755
+ continue;
2756
+ }
2757
+ cleanupErrors.push(result.reason);
2758
+ }
2759
+ }
2760
+ if (deferredCleanup.length > 0) {
2761
+ const failed =
2762
+ cleanupErrors.length > 0
2763
+ ? [Promise.reject(new AggregateError(cleanupErrors, "OMP session cleanup failed"))]
2764
+ : [];
2765
+ throw new OmpCleanupFailure(
2766
+ "OMP session cleanup pending",
2767
+ settleSessionCleanup([...deferredCleanup, ...failed]),
2768
+ this.persistenceSessionId,
2769
+ );
2770
+ }
2771
+ if (cleanupErrors.length > 0) {
2772
+ throw new AggregateError(cleanupErrors, "OMP session cleanup failed");
2773
+ }
2774
+ }
2775
+
2776
+ private publishSessionClosed(error?: { message: string }): void {
2777
+ if (this.sessionClosedPublished) return;
2778
+ this.sessionClosedPublished = true;
2779
+ this.emit({ type: "session.closed", sessionId: this.id, ...(error ? { error } : {}) });
2780
+ }
2781
+
2782
+ private bindRuntime(runtime: OmpRuntimeSession): void {
2783
+ this.unsubscribe();
2784
+ this.runtime = runtime;
2785
+ this.configRefreshAttempts = 0;
2786
+ this.runtimeTurnCompleted = false;
2787
+ const generation = this.generation;
2788
+ this.unsubscribe = runtime.onEvent((event) => {
2789
+ if (generation !== this.generation) return;
2790
+ if (event.type === "host_tool_call" || event.type === "host_tool_cancel") {
2791
+ this.hostTools.handle(event);
2792
+ return;
2793
+ }
2794
+ this.handleRuntimeEvent(event);
2795
+ });
2796
+ }
2797
+
2798
+ private async recoverRuntime(): Promise<void> {
2799
+ if (!this.runtimeDead) return;
2800
+ this.recoveryPromise ??= this.startRecovery();
2801
+ try {
2802
+ await this.recoveryPromise;
2803
+ } finally {
2804
+ this.recoveryPromise = null;
2805
+ }
2806
+ }
2807
+
2808
+ private async startRecovery(): Promise<void> {
2809
+ if (!this.persistSession) {
2810
+ throw new OmpPublicError(
2811
+ "OMP cannot recover a non-persisted session; create a new session instead",
2812
+ );
2813
+ }
2814
+ const expectedSessionId = this.persistSession ? this.nativeSessionId : undefined;
2815
+ if (this.persistSession && !expectedSessionId) {
2816
+ throw new Error("OMP cannot recover because the original native session handle is missing");
2817
+ }
2818
+ const recoverFromNativeConfig = this.recoveryUsesNativeConfig;
2819
+ await this.runtimeDisposal;
2820
+ if (this.closed) throw new Error("OMP session closed while runtime recovery was pending");
2821
+ let recovered: OmpRuntimeSession;
2822
+ try {
2823
+ recovered = await this.runtimeFactory.startSession({
2824
+ ...this.recoveryOptions,
2825
+ ...(recoverFromNativeConfig ? { model: undefined, thinkingOption: undefined } : {}),
2826
+ ...(expectedSessionId ? { resumeSessionId: expectedSessionId } : {}),
2827
+ signal: this.lifetime.signal,
2828
+ });
2829
+ } catch (error) {
2830
+ if (isOmpCleanupFailure(error)) {
2831
+ this.runtimeDisposal = Promise.reject(error);
2832
+ void this.runtimeDisposal.catch(() => undefined);
2833
+ }
2834
+ throw error;
2835
+ }
2836
+ let unsubscribeBootstrap = () => {};
2837
+ let bootstrapConfigRevision = 0;
2838
+ try {
2839
+ unsubscribeBootstrap = recovered.onEvent((event) => {
2840
+ if (event.type === "host_tool_call" || event.type === "host_tool_cancel") {
2841
+ this.hostTools.handle(event);
2842
+ return;
2843
+ }
2844
+ if (isRuntimeConfigEvent(event)) bootstrapConfigRevision += 1;
2845
+ });
2846
+ await this.hostTools.bind(recovered);
2847
+ let state = await recovered.getState();
2848
+ const reconciledConfigRevision = bootstrapConfigRevision;
2849
+ state = await recovered.getState();
2850
+ if (expectedSessionId && state.sessionId !== expectedSessionId) {
2851
+ throw new Error(
2852
+ `OMP resumed native session '${state.sessionId}' instead of '${expectedSessionId}'`,
2853
+ );
2854
+ }
2855
+ if (!expectedSessionId) this.nativeSessionId = state.sessionId;
2856
+ const recoveredModel = state.model ? nativeOmpModelId(state.model) : undefined;
2857
+ if (
2858
+ !recoverFromNativeConfig &&
2859
+ bootstrapConfigRevision === 0 &&
2860
+ recoveredModel !== this.recoveryOptions.model
2861
+ ) {
2862
+ throw new Error("OMP recovered with a different model");
2863
+ }
2864
+ const advertisedModel = state.model
2865
+ ? this.nativeModelsByPublicId.get(ompModelId(state.model))
2866
+ : undefined;
2867
+ if (state.model && !advertisedModel) {
2868
+ throw new Error("OMP recovered with an unadvertised model");
2869
+ }
2870
+ const recoveredThinkingLevel = applicableThinkingLevel(advertisedModel, state.thinkingLevel);
2871
+ if (
2872
+ recoveredThinkingLevel &&
2873
+ !thinkingForModel(advertisedModel).some((option) => option.id === recoveredThinkingLevel)
2874
+ ) {
2875
+ throw new Error("OMP recovered with an unsupported thinking level");
2876
+ }
2877
+ if (this.closed) throw new Error("OMP session closed while runtime recovery was pending");
2878
+ if (!this.hostTools.isBoundTo(recovered)) {
2879
+ throw new Error("OMP host tool bridge detached during recovery");
2880
+ }
2881
+ if (this.subsessions) await recovered.setSubagentSubscription("events");
2882
+ this.generation += 1;
2883
+ this.lastUsage = null;
2884
+ this.runtimeDead = null;
2885
+ this.runtimeDisposal = null;
2886
+ unsubscribeBootstrap();
2887
+ unsubscribeBootstrap = () => {};
2888
+ this.bindRuntime(recovered);
2889
+ if (!this.publishCommittedConfig(state, recovered, this.generation, true)) {
2890
+ throw new Error("OMP session changed while recovery configuration was pending");
2891
+ }
2892
+ this.recoveryUsesNativeConfig = false;
2893
+
2894
+ if (bootstrapConfigRevision !== reconciledConfigRevision) {
2895
+ this.scheduleCommittedConfigRefresh();
2896
+ }
2897
+ } catch (error) {
2898
+ unsubscribeBootstrap();
2899
+ this.hostTools.detach();
2900
+ this.runtimeDisposal = recovered.close();
2901
+ void this.runtimeDisposal.catch(() => undefined);
2902
+ throw error;
2903
+ }
2904
+ }
2905
+ private async routeActivePrompt(
2906
+ clientMessageId: string,
2907
+ payload: OmpPromptPayload,
2908
+ turn: ActiveTurn,
2909
+ autoSteer: boolean,
2910
+ ): Promise<void> {
2911
+ if (!autoSteer) {
2912
+ this.publishSteerFailure(
2913
+ clientMessageId,
2914
+ "OMP already has an active turn; send this message as a steer",
2915
+ );
2916
+ return;
2917
+ }
2918
+ if (turn.starting) await turn.steerReady.promise;
2919
+ await this.steer(clientMessageId, payload, turn);
2920
+ }
2921
+
2922
+ private async steer(
2923
+ clientMessageId: string,
2924
+ payload: OmpPromptPayload,
2925
+ expectedTurn = this.activeTurn,
2926
+ ): Promise<void> {
2927
+ const turn = expectedTurn;
2928
+ if (!this.isSteerableTurn(turn)) {
2929
+ this.publishSteerFailure(clientMessageId, "There is no active OMP turn to steer");
2930
+ return;
2931
+ }
2932
+ const commandName = slashCommandName(payload.text);
2933
+ const slashCommandUnavailable = commandName
2934
+ ? await this.slashSteerUnavailable(commandName)
2935
+ : false;
2936
+ if (!this.isSteerableTurn(turn)) {
2937
+ this.publishSteerFailure(clientMessageId, "There is no active OMP turn to steer");
2938
+ return;
2939
+ }
2940
+ if (slashCommandUnavailable) {
2941
+ this.publishSteerFailure(
2942
+ clientMessageId,
2943
+ "OMP slash commands are unavailable while steering",
2944
+ );
2945
+ return;
2946
+ }
2947
+
2948
+ let materializedPaths: string[] = [];
2949
+ try {
2950
+ const prepared = this.preparePromptPayload(payload, "steer");
2951
+ payload = prepared.payload;
2952
+ materializedPaths = prepared.materializedPaths;
2953
+ } catch (error) {
2954
+ this.imageMaterializer.release(materializedPaths);
2955
+ this.publishSteerFailure(clientMessageId, providerError(error, "OMP steer failed").message);
2956
+ return;
2957
+ }
2958
+ const pending: PendingUser = {
2959
+ clientMessageId,
2960
+ text: payload.text,
2961
+ accepted: false,
2962
+ fallbackOnFinish: false,
2963
+ bufferedEchoes: [],
2964
+ };
2965
+ if (
2966
+ turn.pendingUsers.length >= MAX_PENDING_USERS ||
2967
+ retainedBytes(turn.pendingUsers, MAX_PENDING_USER_BYTES) +
2968
+ boundedJsonBytes(pending, MAX_PENDING_USER_BYTES, MAX_USER_ECHOES) >
2969
+ MAX_PENDING_USER_BYTES
2970
+ ) {
2971
+ this.imageMaterializer.release(materializedPaths);
2972
+ this.publishSteerFailure(clientMessageId, "OMP has too many pending steer messages");
2973
+ return;
2974
+ }
2975
+ turn.pendingUsers.push(pending);
2976
+ turn.steersInFlight += 1;
2977
+ this.cancelLocalOnlyCompletion(turn);
2978
+ try {
2979
+ await this.runtime.steer(payload.text, payload.images);
2980
+ turn.steersInFlight -= 1;
2981
+ if (turn.terminal || turn.terminalizing || this.activeTurn !== turn) {
2982
+ this.imageMaterializer.release(materializedPaths);
2983
+ this.removePendingUser(turn, pending);
2984
+ this.emit({
2985
+ type: "session.prompt_result",
2986
+ sessionId: this.id,
2987
+ clientMessageId,
2988
+ result: {
2989
+ type: "failed",
2990
+ error: { message: "The active OMP turn ended before the steer was accepted" },
2991
+ },
2992
+ });
2993
+ this.resumeAfterFailedSteer(turn);
2994
+ return;
2995
+ }
2996
+ turn.localOnlyDisabled = true;
2997
+ turn.deferredAgentEnd = undefined;
2998
+ this.acceptPendingUser(turn, pending);
2999
+ this.emit({
3000
+ type: "session.prompt_result",
3001
+ sessionId: this.id,
3002
+ clientMessageId,
3003
+ result: { type: "steer", turnId: turn.turnId },
3004
+ });
3005
+ } catch (error) {
3006
+ this.imageMaterializer.release(materializedPaths);
3007
+ turn.steersInFlight -= 1;
3008
+ this.removePendingUser(turn, pending);
3009
+ this.emit({
3010
+ type: "session.prompt_result",
3011
+ sessionId: this.id,
3012
+ clientMessageId,
3013
+ result: { type: "failed", error: providerError(error, "OMP steer failed") },
3014
+ });
3015
+ this.resumeAfterFailedSteer(turn);
3016
+ }
3017
+ }
3018
+
3019
+ private handleRuntimeEvent(event: OmpRpcEvent): void {
3020
+ if (this.closed) return;
3021
+ if (
3022
+ event.type === "subagent_lifecycle" ||
3023
+ event.type === "subagent_progress" ||
3024
+ event.type === "subagent_event"
3025
+ ) {
3026
+ try {
3027
+ this.subsessions?.handle(event);
3028
+ } catch {
3029
+ this.handleRuntimeFailure("OMP subagent event processing failed");
3030
+ }
3031
+ return;
3032
+ }
3033
+ if (event.type === "available_commands_update") {
3034
+ this.replaceSlashCommands(event.commands);
3035
+ this.commandCatalog = event.commands;
3036
+ this.publishCommands(event.commands);
3037
+ return;
3038
+ }
3039
+ if (event.type === "tool_approval_request") {
3040
+ if (!this.capabilities.includes("permission") || !this.runtime.supportsTypedToolApprovals) {
3041
+ this.handleRuntimeFailure("OMP emitted an unnegotiated tool approval request");
3042
+ return;
3043
+ }
3044
+ this.publishToolPermission(event);
3045
+ return;
3046
+ }
3047
+ if (event.type === "tool_approval_cancel") {
3048
+ if (!this.runtime.supportsTypedToolApprovals) {
3049
+ this.handleRuntimeFailure("OMP emitted an unnegotiated tool approval cancellation");
3050
+ return;
3051
+ }
3052
+ this.cancelToolPermission(event);
3053
+ return;
3054
+ }
3055
+ if (event.type === "extension_ui_request") {
3056
+ if (event.method === "cancel") {
3057
+ if (this.pendingFreeformSelection?.nativeSelectId === event.targetId) {
3058
+ this.pendingFreeformSelection = null;
3059
+ }
3060
+ this.resolvePermissionByNativeId(event.targetId ?? "");
3061
+ return;
3062
+ }
3063
+ if (event.method === "input" && this.submitPendingFreeformSelection(event)) return;
3064
+ if (
3065
+ event.method === "select" ||
3066
+ event.method === "confirm" ||
3067
+ event.method === "input" ||
3068
+ event.method === "editor"
3069
+ ) {
3070
+ if (!this.capabilities.includes("permission")) {
3071
+ this.handleRuntimeFailure();
3072
+ return;
3073
+ }
3074
+ this.publishPermission(event);
3075
+ return;
3076
+ }
3077
+
3078
+ if (isPassiveUiMethod(event.method)) {
3079
+ this.projector.projectPassive(event);
3080
+ return;
3081
+ }
3082
+ this.handleRuntimeFailure();
3083
+ return;
3084
+ }
3085
+ if (
3086
+ event.type === "notice" ||
3087
+ event.type === "todo_reminder" ||
3088
+ event.type === "todo_auto_clear" ||
3089
+ event.type === "goal_updated" ||
3090
+ event.type === "auto_retry_start" ||
3091
+ event.type === "auto_retry_end" ||
3092
+ event.type === "compaction_start" ||
3093
+ event.type === "compaction_end" ||
3094
+ event.type === "advisor_yielded"
3095
+ ) {
3096
+ this.projector.projectPassive(event);
3097
+ return;
3098
+ }
3099
+ if (event.type === "process_exit") {
3100
+ this.handleRuntimeFailure();
3101
+ return;
3102
+ }
3103
+ if (event.type === "retry_fallback_applied" || event.type === "retry_fallback_succeeded") {
3104
+ this.projector.projectPassive(event);
3105
+ this.scheduleCommittedConfigRefresh();
3106
+ return;
3107
+ }
3108
+ if (event.type === "model_changed" || event.type === "thinking_level_changed") {
3109
+ this.scheduleCommittedConfigRefresh();
3110
+ return;
3111
+ }
3112
+ const turn = this.activeTurn;
3113
+ if (!turn) {
3114
+ if (event.type === "auto_compaction_start" || event.type === "auto_compaction_end") {
3115
+ this.projector.projectPassive(event);
3116
+ }
3117
+ return;
3118
+ }
3119
+ if (
3120
+ event.type === "agent_end" &&
3121
+ event.requestId !== undefined &&
3122
+ event.requestId !== turn.nativeRequestId
3123
+ ) {
3124
+ return;
3125
+ }
3126
+ if (turn.starting) {
3127
+ if (
3128
+ turn.bufferedEvents.length >= MAX_BUFFERED_TURN_EVENTS ||
3129
+ retainedBytes(turn.bufferedEvents, MAX_BUFFERED_TURN_BYTES) +
3130
+ boundedJsonBytes(
3131
+ event,
3132
+ MAX_BUFFERED_TURN_BYTES,
3133
+ MAX_BUFFERED_VALUE_ITEMS,
3134
+ MAX_BUFFERED_TURN_BYTES,
3135
+ MAX_BUFFERED_VALUE_NODES,
3136
+ ) >
3137
+ MAX_BUFFERED_TURN_BYTES
3138
+ ) {
3139
+ this.handleRuntimeFailure();
3140
+ return;
3141
+ }
3142
+ turn.bufferedEvents.push(event);
3143
+ return;
3144
+ }
3145
+ this.handleTurnEvent(turn, event);
3146
+ }
3147
+
3148
+ private handleTurnEvent(turn: ActiveTurn, event: OmpRpcEvent): void {
3149
+ if (
3150
+ turn.generation !== this.generation ||
3151
+ turn.terminal ||
3152
+ (turn.terminalizing && turn.terminalization !== undefined) ||
3153
+ this.activeTurn !== turn
3154
+ ) {
3155
+ return;
3156
+ }
3157
+ if (
3158
+ event.type === "agent_end" &&
3159
+ event.requestId !== undefined &&
3160
+ event.requestId !== turn.nativeRequestId
3161
+ ) {
3162
+ return;
3163
+ }
3164
+ if (event.type === "message_end") {
3165
+ const entryId = nativeEntryId(event.message);
3166
+ if (!entryId || turn.streamedMessageEntryIds.length >= MAX_AGENT_END_CORRELATION_MESSAGES) {
3167
+ turn.streamedMessageIdentityComplete = false;
3168
+ } else {
3169
+ turn.streamedMessageEntryIds.push(entryId);
3170
+ }
3171
+ turn.completedMessageCount += 1;
3172
+ if (event.message.role === "assistant") {
3173
+ turn.lastCompletedAssistantOutcome = assistantTerminalOutcome(event.message);
3174
+ turn.lastCompletedAssistantEntryId = entryId;
3175
+ }
3176
+ }
3177
+ if (event.type === "prompt_error") {
3178
+ if (event.id !== turn.nativeRequestId) return;
3179
+ const error: ProviderError = {
3180
+ message: this.dataFilter.text(event.error, 4_096),
3181
+ ...(event.code ? { code: this.dataFilter.text(event.code, 256) } : {}),
3182
+ };
3183
+ this.publishPendingUsers(turn);
3184
+ void this.finishTurn(turn, "failed", error);
3185
+ return;
3186
+ }
3187
+ if (event.type === "prompt_result") {
3188
+ if (!event.id || event.id !== turn.nativeRequestId) return;
3189
+ if (event.agentInvoked) {
3190
+ this.markAgentEvidence(turn);
3191
+ if (turn.replayingBufferedEvents) turn.bufferedTerminalOwnershipEvidence = true;
3192
+ else this.markTerminalOwnershipEvidence(turn);
3193
+ return;
3194
+ }
3195
+ if (turn.localOnlyDisabled || turn.steersInFlight > 0) return;
3196
+ if (!turn.nativeActivity && !turn.awaitingPermissionEvidence) {
3197
+ turn.agentInvoked = false;
3198
+ turn.localOnlyEligible = true;
3199
+ this.scheduleLocalOnlyCompletion(turn);
3200
+ }
3201
+ return;
3202
+ }
3203
+ if (event.type === "auto_compaction_start") {
3204
+ turn.nativeActivity = true;
3205
+ this.cancelLocalOnlyCompletion(turn);
3206
+ this.startCompaction(turn, "auto", event.action);
3207
+ return;
3208
+ }
3209
+ if (event.type === "auto_compaction_end") {
3210
+ if (this.discardedCompactionEnds > 0) {
3211
+ this.discardedCompactionEnds -= 1;
3212
+ return;
3213
+ }
3214
+ const operation = this.activeCompaction;
3215
+ if (
3216
+ operation?.trigger !== "auto" ||
3217
+ operation.turnId !== turn.turnId ||
3218
+ operation.generation !== turn.generation
3219
+ ) {
3220
+ return;
3221
+ }
3222
+ if (event.action !== undefined && operation.action !== event.action) {
3223
+ this.retireCompaction("OMP emitted overlapping compactions");
3224
+ this.discardedCompactionEnds = 1;
3225
+ return;
3226
+ }
3227
+ if (event.willRetry) {
3228
+ operation.retrying = true;
3229
+ return;
3230
+ }
3231
+ const state = event.aborted
3232
+ ? "canceled"
3233
+ : event.errorMessage
3234
+ ? "failed"
3235
+ : event.skipped
3236
+ ? "skipped"
3237
+ : "completed";
3238
+ this.finishCompaction(state, {
3239
+ tokensBefore: event.result?.tokensBefore ?? event.result?.preTokens,
3240
+ message: event.errorMessage,
3241
+ });
3242
+ this.scheduleUsagePoll(turn, USAGE_REFRESH_MS);
3243
+ return;
3244
+ }
3245
+ if (event.type === "agent_end" && turn.manualCompactionPending) return;
3246
+ if (event.type === "tool_execution_end" && event.toolName === "ask_user") {
3247
+ this.pendingFreeformSelection = null;
3248
+ }
3249
+ if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
3250
+ try {
3251
+ this.subsessions?.observeSessionEvent(this.id, event);
3252
+ } catch {
3253
+ this.handleRuntimeFailure("OMP subagent dispatch tracking failed");
3254
+ return;
3255
+ }
3256
+ }
3257
+ if (isNativeTurnActivity(event)) {
3258
+ turn.nativeActivity = true;
3259
+ this.cancelLocalOnlyCompletion(turn);
3260
+ }
3261
+ if (event.type === "message_end" && event.message.role === "user") {
3262
+ this.markAgentEvidence(turn);
3263
+ this.projectUserEcho(turn, event.message);
3264
+ return;
3265
+ }
3266
+ if (
3267
+ (event.type === "message_start" ||
3268
+ event.type === "message_update" ||
3269
+ event.type === "message_end") &&
3270
+ event.message.role === "assistant"
3271
+ ) {
3272
+ turn.awaitingPermissionEvidence = false;
3273
+ }
3274
+ if (event.type === "agent_end") {
3275
+ if (event.isTerminal === false) {
3276
+ turn.completedMessageCount = 0;
3277
+ turn.streamedMessageEntryIds.length = 0;
3278
+ turn.streamedMessageIdentityComplete = true;
3279
+ turn.lastCompletedAssistantOutcome = undefined;
3280
+ turn.lastCompletedAssistantEntryId = undefined;
3281
+ return;
3282
+ }
3283
+ if (event.requestId !== undefined) this.markTerminalOwnershipEvidence(turn);
3284
+ if (
3285
+ !turn.interrupted &&
3286
+ turn.terminalOwnershipRequired &&
3287
+ !turn.terminalOwnershipEvidence &&
3288
+ !(turn.replayingBufferedEvents && turn.bufferedTerminalOwnershipEvidence)
3289
+ ) {
3290
+ this.scheduleTerminalOwnershipTimeout(turn);
3291
+ return;
3292
+ }
3293
+ const hasAssistantEvidence =
3294
+ event.messages?.some((message) => message.role === "assistant") ?? false;
3295
+ if (turn.awaitingPermissionEvidence && !hasAssistantEvidence) {
3296
+ turn.deferredAgentEnd = event;
3297
+ return;
3298
+ }
3299
+ if (hasAssistantEvidence) turn.awaitingPermissionEvidence = false;
3300
+ if (turn.terminalizing) {
3301
+ turn.deferredAgentEnd = event;
3302
+ return;
3303
+ }
3304
+ if (turn.steersInFlight > 0) {
3305
+ turn.deferredAgentEnd = event;
3306
+ return;
3307
+ }
3308
+ this.beginTerminalization(turn, event);
3309
+ return;
3310
+ }
3311
+ if (isNativeTurnActivity(event)) this.markAgentEvidence(turn);
3312
+ if (event.type === "message_end" && event.message.role === "user") {
3313
+ this.markAgentEvidence(turn);
3314
+ this.projectUserEcho(turn, event.message);
3315
+ return;
3316
+ }
3317
+ this.projector.project(event, turn.turnId);
3318
+ }
3319
+
3320
+ private projectUserEcho(turn: ActiveTurn, message: OmpMessage): void {
3321
+ turn.userEchoObserved = true;
3322
+ const entryId = nativeEntryId(message);
3323
+ if (
3324
+ entryId &&
3325
+ this.seenEntryIds.has(entryId) &&
3326
+ !this.unclaimedBranchEntries.some((entry) => entry.entryId === entryId)
3327
+ ) {
3328
+ return;
3329
+ }
3330
+ if (
3331
+ turn.userEchoes.length >= MAX_USER_ECHOES ||
3332
+ retainedBytes(turn.userEchoes, MAX_USER_ECHO_BYTES) +
3333
+ boundedJsonBytes(message, MAX_USER_ECHO_BYTES, MAX_USER_ECHOES) >
3334
+ MAX_USER_ECHO_BYTES
3335
+ ) {
3336
+ this.handleRuntimeFailure();
3337
+ return;
3338
+ }
3339
+ turn.userEchoes.push(message);
3340
+ this.drainUserEchoes(turn);
3341
+ }
3342
+
3343
+ private drainUserEchoes(turn: ActiveTurn): void {
3344
+ if (turn.userCorrelationActive) return;
3345
+ turn.userCorrelationActive = true;
3346
+ const correlation = this.correlateUserEchoes(turn);
3347
+ turn.userLookups.add(correlation);
3348
+ void correlation.finally(() => {
3349
+ turn.userLookups.delete(correlation);
3350
+ turn.userCorrelationActive = false;
3351
+ if (!turn.terminal && !turn.terminalizing && turn.userEchoes.length > 0) {
3352
+ this.drainUserEchoes(turn);
3353
+ }
3354
+ });
3355
+ }
3356
+
3357
+ private async correlateUserEchoes(turn: ActiveTurn): Promise<void> {
3358
+ while (turn.userEchoes.length > 0) {
3359
+ const message = turn.userEchoes[0];
3360
+ if (!message) return;
3361
+ const entryId = nativeEntryId(message);
3362
+ if (
3363
+ entryId &&
3364
+ this.emittedEntryIds.has(entryId) &&
3365
+ !this.unclaimedBranchEntries.some((entry) => entry.entryId === entryId)
3366
+ ) {
3367
+ turn.userEchoes.shift();
3368
+ continue;
3369
+ }
3370
+ const pending = turn.pendingUsers[0];
3371
+ if (!pending) {
3372
+ turn.userEchoes.shift();
3373
+ continue;
3374
+ }
3375
+ if (!pending.accepted) {
3376
+ if (
3377
+ pending.bufferedEchoes.length + turn.userEchoes.length > MAX_USER_ECHOES ||
3378
+ retainedBytes(pending.bufferedEchoes, MAX_USER_ECHO_BYTES) +
3379
+ retainedBytes(turn.userEchoes, MAX_USER_ECHO_BYTES) >
3380
+ MAX_USER_ECHO_BYTES
3381
+ ) {
3382
+ this.handleRuntimeFailure();
3383
+ return;
3384
+ }
3385
+ pending.bufferedEchoes.push(...turn.userEchoes.splice(0));
3386
+ return;
3387
+ }
3388
+ let resolvedId = entryId ?? this.claimUnclaimedBranchEntry(turn, pending.text);
3389
+ if (!resolvedId && (await this.refreshBranchEntries(turn, pending))) {
3390
+ resolvedId = this.claimUnclaimedBranchEntry(turn, pending.text);
3391
+ }
3392
+ if (
3393
+ this.closed ||
3394
+ turn.terminal ||
3395
+ this.activeTurn !== turn ||
3396
+ turn.pendingUsers[0] !== pending
3397
+ ) {
3398
+ return;
3399
+ }
3400
+ turn.userEchoes.shift();
3401
+ if (!resolvedId) return;
3402
+ turn.pendingUsers.shift();
3403
+ this.publishCorrelatedUser(turn, pending, resolvedId);
3404
+ }
3405
+ }
3406
+ private async refreshBranchEntries(turn: ActiveTurn, pending: PendingUser): Promise<boolean> {
3407
+ const runtime = this.runtime;
3408
+ try {
3409
+ const messages = await runtime.getBranchMessages();
3410
+ if (
3411
+ this.closed ||
3412
+ turn.terminal ||
3413
+ this.activeTurn !== turn ||
3414
+ turn.pendingUsers[0] !== pending ||
3415
+ turn.generation !== this.generation ||
3416
+ runtime !== this.runtime
3417
+ ) {
3418
+ return false;
3419
+ }
3420
+ if (
3421
+ messages.length > MAX_UNCLAIMED_BRANCH_ENTRIES ||
3422
+ retainedBytes(messages, MAX_UNCLAIMED_BRANCH_BYTES) === Number.POSITIVE_INFINITY
3423
+ ) {
3424
+ this.quarantineBranchEntries();
3425
+ return false;
3426
+ }
3427
+ const unseen: Array<{ entryId: string; text: string }> = [];
3428
+ const snapshotIds = new Set<string>();
3429
+ for (const message of messages) {
3430
+ if (snapshotIds.has(message.entryId)) {
3431
+ this.quarantineBranchEntries();
3432
+ return false;
3433
+ }
3434
+ snapshotIds.add(message.entryId);
3435
+ if (!this.branchEntryIds.has(message.entryId)) unseen.push(message);
3436
+ }
3437
+ if (!this.branchWatermarkValid) {
3438
+ this.unclaimedBranchEntries.length = 0;
3439
+ this.branchWatermarkValid = true;
3440
+ } else if (
3441
+ unseen.length <= MAX_UNCLAIMED_BRANCH_ENTRIES - this.unclaimedBranchEntries.length &&
3442
+ this.branchEntryIds.size + unseen.length <= MAX_UNCLAIMED_BRANCH_ENTRIES &&
3443
+ retainedBytes(this.unclaimedBranchEntries, MAX_UNCLAIMED_BRANCH_BYTES) +
3444
+ retainedBytes(unseen, MAX_UNCLAIMED_BRANCH_BYTES) <=
3445
+ MAX_UNCLAIMED_BRANCH_BYTES
3446
+ ) {
3447
+ this.unclaimedBranchEntries.push(...unseen);
3448
+ } else {
3449
+ this.quarantineBranchEntries();
3450
+ return false;
3451
+ }
3452
+ for (const message of messages) {
3453
+ this.branchEntryIds.add(message.entryId);
3454
+ this.seenEntryIds.add(message.entryId);
3455
+ }
3456
+ return true;
3457
+ } catch {
3458
+ if (
3459
+ !this.closed &&
3460
+ !turn.terminal &&
3461
+ this.activeTurn === turn &&
3462
+ turn.pendingUsers[0] === pending &&
3463
+ turn.generation === this.generation &&
3464
+ runtime === this.runtime
3465
+ ) {
3466
+ this.quarantineBranchEntries();
3467
+ }
3468
+ return false;
3469
+ }
3470
+ }
3471
+
3472
+ private claimUnclaimedBranchEntry(turn: ActiveTurn, text: string): string | undefined {
3473
+ const index = this.unclaimedBranchEntries.findIndex((entry) => entry.text === text);
3474
+ if (index < 0) return undefined;
3475
+ let matches = 0;
3476
+ let expected = 0;
3477
+ for (const entry of this.unclaimedBranchEntries) if (entry.text === text) matches += 1;
3478
+ for (const pending of turn.pendingUsers) {
3479
+ if (pending.accepted && pending.text === text) expected += 1;
3480
+ }
3481
+ if (matches > expected) {
3482
+ this.quarantineBranchEntries();
3483
+ return undefined;
3484
+ }
3485
+ return this.unclaimedBranchEntries.splice(index, 1)[0]?.entryId;
3486
+ }
3487
+
3488
+ private quarantineBranchEntries(): void {
3489
+ this.unclaimedBranchEntries.length = 0;
3490
+ this.branchEntryIds.clear();
3491
+ this.branchWatermarkValid = false;
3492
+ }
3493
+
3494
+ private isSteerableTurn(turn: ActiveTurn | null): turn is ActiveTurn {
3495
+ return (
3496
+ turn !== null &&
3497
+ this.activeTurn === turn &&
3498
+ !turn.terminal &&
3499
+ !turn.terminalizing &&
3500
+ !turn.deferredAgentEnd &&
3501
+ !turn.agentEndPending &&
3502
+ !turn.manualCompactionPending &&
3503
+ turn.started
3504
+ );
3505
+ }
3506
+
3507
+ private publishSteerFailure(clientMessageId: string, message: string): void {
3508
+ this.emit({
3509
+ type: "session.prompt_result",
3510
+ sessionId: this.id,
3511
+ clientMessageId,
3512
+ result: { type: "failed", error: { message } },
3513
+ });
3514
+ }
3515
+
3516
+ private submitPendingFreeformSelection(
3517
+ request: Extract<OmpQuestionRequest, { method: "input" }>,
3518
+ ): boolean {
3519
+ const pending = this.pendingFreeformSelection;
3520
+ if (!pending) return false;
3521
+ this.pendingFreeformSelection = null;
3522
+ if (
3523
+ pending.generation !== this.generation ||
3524
+ pending.runtime !== this.runtime ||
3525
+ (pending.turnId !== undefined && this.activeTurn?.turnId !== pending.turnId)
3526
+ ) {
3527
+ return false;
3528
+ }
3529
+ void pending.runtime
3530
+ .respondToExtensionUi({ type: "extension_ui_response", id: request.id, value: pending.value })
3531
+ .catch(() => this.handleRuntimeFailure("OMP freeform response failed"));
3532
+ return true;
3533
+ }
3534
+ private replaceSlashCommands(commands: OmpAvailableCommand[]): void {
3535
+ this.slashCommands.clear();
3536
+ for (const command of [...OMP_BUILTIN_COMMANDS, ...commands]) {
3537
+ if (isSafeCommandName(command.name)) this.slashCommands.add(command.name);
3538
+ for (const alias of command.aliases ?? []) {
3539
+ if (isSafeCommandName(alias)) this.slashCommands.add(alias);
3540
+ }
3541
+ }
3542
+ this.commandDiscoveryAvailable = true;
3543
+ }
3544
+
3545
+ private publishCommands(commands: OmpAvailableCommand[]): void {
3546
+ const merged = new Map(OMP_BUILTIN_COMMANDS.map((command) => [command.name, command] as const));
3547
+ for (const command of commands) merged.set(command.name, command);
3548
+ this.emit({
3549
+ type: "session.commands",
3550
+ sessionId: this.id,
3551
+ commands: [...merged.values()]
3552
+ .filter((command) => isSafeCommandName(command.name))
3553
+ .map((command) => {
3554
+ const name = this.dataFilter.text(command.name, 256);
3555
+ return {
3556
+ name,
3557
+ description: this.dataFilter.text(command.description ?? `Run /${name}`, 4_096),
3558
+ ...(command.input?.hint
3559
+ ? { argumentHint: this.dataFilter.text(command.input.hint, 1_024) }
3560
+ : {}),
3561
+ };
3562
+ }),
3563
+ });
3564
+ }
3565
+ private toolPermissionDetail(
3566
+ request: OmpToolApprovalRequest,
3567
+ ): ProviderToolCallDetail | undefined {
3568
+ switch (request.identity.kind) {
3569
+ case "shell":
3570
+ return {
3571
+ type: "shell",
3572
+ command: this.dataFilter.text(request.identity.command, 24 * 1024),
3573
+ };
3574
+ case "edit":
3575
+ return {
3576
+ type: "edit",
3577
+ filePath: this.dataFilter.text(request.identity.paths[0] ?? "", 4_096),
3578
+ newString: this.dataFilter.text(request.identity.content, 20 * 1024),
3579
+ };
3580
+ case "write":
3581
+ return {
3582
+ type: "write",
3583
+ filePath: this.dataFilter.text(request.identity.path, 4_096),
3584
+ content: this.dataFilter.text(request.identity.content, 20 * 1024),
3585
+ };
3586
+ case "other":
3587
+ return;
3588
+ }
3589
+ }
3590
+
3591
+ private publishToolPermission(request: OmpToolApprovalRequest): void {
3592
+ const fingerprint = createHash("sha256").update(JSON.stringify(request)).digest("base64url");
3593
+ if (this.resolvedToolApprovalIds.has(request.id)) {
3594
+ this.handleRuntimeFailure("OMP reused a resolved tool approval identifier");
3595
+ return;
3596
+ }
3597
+ for (const pending of [
3598
+ ...this.pendingToolPermissions.values(),
3599
+ ...this.inFlightToolPermissions.values(),
3600
+ ]) {
3601
+ if (pending.nativeId !== request.id) continue;
3602
+ if (pending.fingerprint !== fingerprint || pending.toolCallId !== request.toolCallId) {
3603
+ this.handleRuntimeFailure("OMP changed a pending tool approval request");
3604
+ }
3605
+ return;
3606
+ }
3607
+ const pendingCount =
3608
+ this.pendingPermissions.size +
3609
+ this.inFlightPermissions.size +
3610
+ this.pendingToolPermissions.size +
3611
+ this.inFlightToolPermissions.size;
3612
+ if (pendingCount >= MAX_PENDING_PERMISSIONS) {
3613
+ this.resolvedToolApprovalIds.add(request.id);
3614
+ void this.runtime
3615
+ .respondToToolApproval({
3616
+ type: "tool_approval_response",
3617
+ id: request.id,
3618
+ toolCallId: request.toolCallId,
3619
+ cancelled: true,
3620
+ })
3621
+ .catch(() => this.handleRuntimeFailure());
3622
+ return;
3623
+ }
3624
+
3625
+ this.permissionSequence += 1;
3626
+ const permissionId = `omp:permission:${this.permissionNamespace}:${this.permissionSequence}`;
3627
+ const detail = this.toolPermissionDetail(request);
3628
+ const filteredInput = this.dataFilter.json(request.input, 8 * 1024, 32 * 1024);
3629
+ const input =
3630
+ filteredInput && typeof filteredInput === "object" && !Array.isArray(filteredInput)
3631
+ ? filteredInput
3632
+ : {};
3633
+ const descriptionParts = [
3634
+ request.detail.reason,
3635
+ ...request.detail.lines,
3636
+ ...(request.detail.providerSafetyChecks ?? []),
3637
+ ].filter((part): part is string => Boolean(part));
3638
+ const publicRequest = {
3639
+ id: permissionId,
3640
+ name: `omp.${this.dataFilter.text(request.toolName, 256)}`,
3641
+ kind: "tool" as const,
3642
+ title: `Allow ${this.dataFilter.text(request.toolName, 256)}?`,
3643
+ ...(descriptionParts.length > 0
3644
+ ? { description: this.dataFilter.text(descriptionParts.join("\n"), 16 * 1024) }
3645
+ : {}),
3646
+ input: {
3647
+ ...input,
3648
+ identity: this.dataFilter.json(request.identity, 20 * 1024, 32 * 1024),
3649
+ tier: request.tier,
3650
+ toolCallId: request.toolCallId,
3651
+ },
3652
+ ...(detail ? { detail } : {}),
3653
+ actions: [
3654
+ { id: "allow", label: "Allow", behavior: "allow" as const, variant: "primary" as const },
3655
+ { id: "deny", label: "Deny", behavior: "deny" as const, variant: "danger" as const },
3656
+ ],
3657
+ metadata: {
3658
+ tier: request.tier,
3659
+ redacted: request.detail.redacted,
3660
+ truncated: request.detail.truncated,
3661
+ redactedFields: request.detail.redactedFields,
3662
+ truncatedFields: request.detail.truncatedFields,
3663
+ },
3664
+ };
3665
+ const pending: PendingToolPermission = {
3666
+ nativeId: request.id,
3667
+ toolCallId: request.toolCallId,
3668
+ fingerprint,
3669
+ retainedBytes: boundedJsonBytes(
3670
+ publicRequest,
3671
+ MAX_PENDING_PERMISSION_BYTES,
3672
+ 512,
3673
+ MAX_PENDING_PERMISSION_BYTES,
3674
+ 4_096,
3675
+ ),
3676
+ generation: this.generation,
3677
+ runtime: this.runtime,
3678
+ ...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
3679
+ ...(request.timeout !== undefined ? { expiresAt: Date.now() + request.timeout } : {}),
3680
+ };
3681
+ let retainedBytes = pending.retainedBytes;
3682
+ for (const item of this.pendingPermissions.values()) retainedBytes += item.retainedBytes;
3683
+ for (const item of this.inFlightPermissions.values()) retainedBytes += item.retainedBytes;
3684
+ for (const item of this.pendingToolPermissions.values()) retainedBytes += item.retainedBytes;
3685
+ for (const item of this.inFlightToolPermissions.values()) retainedBytes += item.retainedBytes;
3686
+ if (
3687
+ pending.retainedBytes === Number.POSITIVE_INFINITY ||
3688
+ retainedBytes > MAX_PENDING_PERMISSION_BYTES
3689
+ ) {
3690
+ void this.runtime
3691
+ .respondToToolApproval({
3692
+ type: "tool_approval_response",
3693
+ id: request.id,
3694
+ toolCallId: request.toolCallId,
3695
+ cancelled: true,
3696
+ })
3697
+ .catch(() => this.handleRuntimeFailure());
3698
+ this.resolvedToolApprovalIds.add(request.id);
3699
+ return;
3700
+ }
3701
+ this.pendingToolPermissions.set(permissionId, pending);
3702
+ this.armToolPermissionTimeout(permissionId, pending);
3703
+ this.projector.markAskPermissionRendered();
3704
+ if (this.activeTurn) {
3705
+ this.activeTurn.awaitingPermissionEvidence = true;
3706
+ this.markAgentEvidence(this.activeTurn);
3707
+ }
3708
+ this.emit({ type: "session.permission", sessionId: this.id, request: publicRequest });
3709
+ }
3710
+
3711
+ private cancelToolPermission(request: OmpToolApprovalCancel): void {
3712
+ for (const permissions of [this.pendingToolPermissions, this.inFlightToolPermissions]) {
3713
+ for (const [permissionId, pending] of permissions) {
3714
+ if (pending.nativeId !== request.targetId) continue;
3715
+ if (pending.toolCallId !== request.toolCallId) {
3716
+ this.handleRuntimeFailure("OMP tool approval cancellation did not match its tool call");
3717
+ return;
3718
+ }
3719
+ permissions.delete(permissionId);
3720
+ if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
3721
+ this.resolvedToolApprovalIds.add(pending.nativeId);
3722
+ this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
3723
+ this.reevaluateDeferredPermissionTerminal();
3724
+ return;
3725
+ }
3726
+ }
3727
+ if (!this.resolvedToolApprovalIds.has(request.targetId)) {
3728
+ this.handleRuntimeFailure("OMP canceled an unknown tool approval request");
3729
+ }
3730
+ }
3731
+
3732
+ private publishPermission(request: OmpQuestionRequest): void {
3733
+ if (request.method === "select" && !request.options?.length) {
3734
+ this.handleRuntimeFailure();
3735
+ return;
3736
+ }
3737
+ const fingerprint = permissionFingerprint(request);
3738
+ let existingId: string | undefined;
3739
+ let existingPending: PendingPermission | undefined;
3740
+ for (const [permissionId, pending] of this.pendingPermissions) {
3741
+ if (pending.nativeId !== request.id) continue;
3742
+ if (pending.fingerprint === fingerprint) {
3743
+ existingId = permissionId;
3744
+ existingPending = pending;
3745
+ } else {
3746
+ this.pendingPermissions.delete(permissionId);
3747
+ if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
3748
+ this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
3749
+ }
3750
+ break;
3751
+ }
3752
+ if (!existingId) {
3753
+ for (const pending of this.inFlightPermissions.values()) {
3754
+ if (pending.nativeId !== request.id) continue;
3755
+ if (pending.fingerprint !== fingerprint) this.handleRuntimeFailure();
3756
+ return;
3757
+ }
3758
+ }
3759
+ if (
3760
+ !existingId &&
3761
+ this.pendingPermissions.size +
3762
+ this.inFlightPermissions.size +
3763
+ this.pendingToolPermissions.size +
3764
+ this.inFlightToolPermissions.size >=
3765
+ MAX_PENDING_PERMISSIONS
3766
+ ) {
3767
+ this.rejectPermissionRequest(request, "Too many OMP questions are already pending");
3768
+ return;
3769
+ }
3770
+ if (existingPending?.timer !== undefined) this.scheduler.clear(existingPending.timer);
3771
+ if (!existingId) this.permissionSequence += 1;
3772
+ const id =
3773
+ existingId ?? `omp:permission:${this.permissionNamespace}:${this.permissionSequence}`;
3774
+ const header = this.dataFilter.text(request.title ?? "OMP question", 4_096);
3775
+ const freeformSentinel =
3776
+ request.method === "select" && request.options.includes(OMP_ASK_USER_FREEFORM_SENTINEL)
3777
+ ? OMP_ASK_USER_FREEFORM_SENTINEL
3778
+ : undefined;
3779
+ const optionValues = new Map<string, string>();
3780
+ const displayValues = new Map<string, string>();
3781
+ const usedOptionLabels = new Set<string>();
3782
+ const optionDetails = request.method === "select" ? request.optionDetails : undefined;
3783
+ const selectableOptions =
3784
+ request.method === "select"
3785
+ ? request.options.flatMap((nativeValue, index) =>
3786
+ nativeValue === freeformSentinel ? [] : [{ nativeValue, index }],
3787
+ )
3788
+ : [];
3789
+ const options =
3790
+ request.method === "select"
3791
+ ? selectableOptions.map(({ nativeValue, index }) => {
3792
+ const baseLabel = this.dataFilter.text(nativeValue, 4_096);
3793
+ let label = baseLabel;
3794
+ let suffix = 2;
3795
+ while (usedOptionLabels.has(label)) {
3796
+ label = `${baseLabel} (${suffix})`;
3797
+ suffix += 1;
3798
+ }
3799
+ usedOptionLabels.add(label);
3800
+ const value = `${id}:option:${index}`;
3801
+ optionValues.set(value, nativeValue);
3802
+ displayValues.set(label, nativeValue);
3803
+ return {
3804
+ label,
3805
+ value,
3806
+ ...(optionDetails?.[index]?.description
3807
+ ? {
3808
+ description: this.dataFilter.text(
3809
+ optionDetails[index]?.description ?? "",
3810
+ 16_384,
3811
+ ),
3812
+ }
3813
+ : {}),
3814
+ };
3815
+ })
3816
+ : undefined;
3817
+ const questionOptions =
3818
+ request.method === "confirm" ? [{ label: "Yes" }, { label: "No" }] : (options ?? []);
3819
+ const actions =
3820
+ request.method === "select"
3821
+ ? [
3822
+ ...(options ?? []).map((option) => ({
3823
+ id: option.value,
3824
+ label: option.label,
3825
+ behavior: "allow" as const,
3826
+ variant: "secondary" as const,
3827
+ })),
3828
+ {
3829
+ id: "cancel",
3830
+ label: "Cancel",
3831
+ behavior: "deny" as const,
3832
+ variant: "secondary" as const,
3833
+ },
3834
+ ]
3835
+ : [
3836
+ {
3837
+ id: "submit",
3838
+ label: request.method === "confirm" ? "Confirm" : "Submit",
3839
+ behavior: "allow" as const,
3840
+ variant: "primary" as const,
3841
+ },
3842
+ {
3843
+ id: "cancel",
3844
+ label: "Cancel",
3845
+ behavior: "deny" as const,
3846
+ variant: "secondary" as const,
3847
+ },
3848
+ ];
3849
+ const pending: PendingPermission = {
3850
+ nativeId: request.id,
3851
+ header,
3852
+ fingerprint,
3853
+ optionValues,
3854
+ actionBehaviors: new Map(actions.map((action) => [action.id, action.behavior])),
3855
+ displayValues,
3856
+ generation: this.generation,
3857
+ runtime: this.runtime,
3858
+ request,
3859
+ retainedBytes: boundedJsonBytes(
3860
+ { request, header, options: questionOptions, actions },
3861
+ MAX_PENDING_PERMISSION_BYTES,
3862
+ 512,
3863
+ MAX_PENDING_PERMISSION_BYTES,
3864
+ 4_096,
3865
+ ),
3866
+ ...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
3867
+ ...(request.timeout !== undefined ? { expiresAt: Date.now() + request.timeout } : {}),
3868
+ ...(freeformSentinel ? { freeformSentinel } : {}),
3869
+ };
3870
+ let retainedPermissionBytes = pending.retainedBytes;
3871
+ for (const [permissionId, retained] of this.pendingPermissions) {
3872
+ if (permissionId !== existingId) retainedPermissionBytes += retained.retainedBytes;
3873
+ }
3874
+ for (const retained of this.inFlightPermissions.values()) {
3875
+ retainedPermissionBytes += retained.retainedBytes;
3876
+ }
3877
+ for (const retained of this.pendingToolPermissions.values()) {
3878
+ retainedPermissionBytes += retained.retainedBytes;
3879
+ }
3880
+ for (const retained of this.inFlightToolPermissions.values()) {
3881
+ retainedPermissionBytes += retained.retainedBytes;
3882
+ }
3883
+ if (
3884
+ pending.retainedBytes === Number.POSITIVE_INFINITY ||
3885
+ retainedPermissionBytes > MAX_PENDING_PERMISSION_BYTES
3886
+ ) {
3887
+ if (existingId) {
3888
+ this.pendingPermissions.delete(existingId);
3889
+ this.emit({
3890
+ type: "session.permission_resolved",
3891
+ sessionId: this.id,
3892
+ permissionId: existingId,
3893
+ });
3894
+ }
3895
+ this.rejectPermissionRequest(request, "OMP question data exceeded the pending input budget");
3896
+ return;
3897
+ }
3898
+ this.pendingPermissions.set(id, pending);
3899
+ this.armPermissionTimeout(id, pending);
3900
+ this.projector.markAskPermissionRendered();
3901
+ if (this.activeTurn) {
3902
+ this.activeTurn.awaitingPermissionEvidence = true;
3903
+ this.markAgentEvidence(this.activeTurn);
3904
+ }
3905
+ this.emit({
3906
+ type: "session.permission",
3907
+ sessionId: this.id,
3908
+ request: {
3909
+ id,
3910
+ name: `omp.${request.method}`,
3911
+ kind: "question",
3912
+ title: header,
3913
+ ...(request.method === "confirm"
3914
+ ? { description: this.dataFilter.text(request.message, 64 * 1024) }
3915
+ : {}),
3916
+ input: {
3917
+ questions: [
3918
+ {
3919
+ header,
3920
+ question: this.dataFilter.text(
3921
+ request.method === "confirm" ? request.message : request.title,
3922
+ 64 * 1024,
3923
+ ),
3924
+ options: questionOptions,
3925
+ multiSelect: false,
3926
+ ...(freeformSentinel ? { allowOther: true } : {}),
3927
+ ...(request.method === "input" && request.placeholder
3928
+ ? { placeholder: this.dataFilter.text(request.placeholder, 4_096) }
3929
+ : {}),
3930
+ ...((request.method === "input" || request.method === "editor") && request.prefill
3931
+ ? { prefill: this.dataFilter.text(request.prefill) }
3932
+ : {}),
3933
+ },
3934
+ ],
3935
+ },
3936
+ actions,
3937
+ },
3938
+ });
3939
+ }
3940
+
3941
+ private rejectPermissionRequest(request: OmpQuestionRequest, description: string): void {
3942
+ this.emit({
3943
+ type: "session.notice",
3944
+ sessionId: this.id,
3945
+ notice: {
3946
+ id: `omp:permission-rejected:${this.permissionSequence + 1}`,
3947
+ severity: "warning",
3948
+ title: "OMP question canceled",
3949
+ description,
3950
+ },
3951
+ });
3952
+ void this.runtime
3953
+ .respondToExtensionUi({ type: "extension_ui_response", id: request.id, cancelled: true })
3954
+ .catch(() => this.handleRuntimeFailure());
3955
+ }
3956
+
3957
+ private freeformSelection(
3958
+ pending: PendingPermission,
3959
+ response: ProviderPermissionResponse,
3960
+ ): string | undefined {
3961
+ if (
3962
+ pending.request.method !== "select" ||
3963
+ !pending.freeformSentinel ||
3964
+ response.behavior !== "allow" ||
3965
+ response.selectedActionId !== undefined
3966
+ ) {
3967
+ return;
3968
+ }
3969
+ const answers = response.updatedInput?.answers;
3970
+ const answer =
3971
+ answers && typeof answers === "object" && !Array.isArray(answers)
3972
+ ? answers[pending.header]
3973
+ : undefined;
3974
+ const value = Array.isArray(answer) ? answer[0] : answer;
3975
+ if (
3976
+ typeof value !== "string" ||
3977
+ pending.optionValues.has(value) ||
3978
+ pending.displayValues.has(value)
3979
+ ) {
3980
+ return;
3981
+ }
3982
+ if (
3983
+ value === pending.freeformSentinel ||
3984
+ value.trim().length === 0 ||
3985
+ value.includes("\0") ||
3986
+ utf8Bytes(value) > MAX_FREEFORM_RESPONSE_BYTES
3987
+ ) {
3988
+ throw new OmpPublicError("OMP freeform response is invalid");
3989
+ }
3990
+ return value;
3991
+ }
3992
+
3993
+ private extensionUiResponse(
3994
+ pending: PendingPermission,
3995
+ response: ProviderPermissionResponse,
3996
+ ): OmpExtensionUiResponse {
3997
+ if (response.selectedActionId !== undefined) {
3998
+ const expectedBehavior = pending.actionBehaviors.get(response.selectedActionId);
3999
+ if (expectedBehavior === undefined || expectedBehavior !== response.behavior) {
4000
+ throw new OmpPublicError("OMP permission action is invalid");
4001
+ }
4002
+ }
4003
+ const { nativeId, request, header } = pending;
4004
+ if (request.method === "confirm") {
4005
+ if (response.behavior === "deny") {
4006
+ return { type: "extension_ui_response", id: nativeId, confirmed: false };
4007
+ }
4008
+ const answers = response.updatedInput?.answers;
4009
+ const answer =
4010
+ answers && typeof answers === "object" && !Array.isArray(answers)
4011
+ ? answers[header]
4012
+ : undefined;
4013
+ return {
4014
+ type: "extension_ui_response",
4015
+ id: nativeId,
4016
+ confirmed: typeof answer === "string" ? /^yes$/iu.test(answer.trim()) : true,
4017
+ };
4018
+ }
4019
+ this.reevaluateDeferredPermissionTerminal();
4020
+ if (response.behavior === "deny") {
4021
+ return { type: "extension_ui_response", id: nativeId, cancelled: true };
4022
+ }
4023
+ const selectedValue = response.selectedActionId
4024
+ ? pending.optionValues.get(response.selectedActionId)
4025
+ : undefined;
4026
+ if (selectedValue !== undefined) {
4027
+ return { type: "extension_ui_response", id: nativeId, value: selectedValue };
4028
+ }
4029
+ const answers = response.updatedInput?.answers;
4030
+ if (!answers || typeof answers !== "object" || Array.isArray(answers)) {
4031
+ throw new OmpPublicError("OMP question response requires answers");
4032
+ }
4033
+ const answer = answers[header];
4034
+ const publicValue = Array.isArray(answer) ? answer[0] : answer;
4035
+ if (typeof publicValue !== "string") {
4036
+ throw new OmpPublicError("OMP question response is invalid");
4037
+ }
4038
+ if (
4039
+ request.method === "select" &&
4040
+ pending.freeformSentinel &&
4041
+ publicValue !== pending.freeformSentinel &&
4042
+ !pending.displayValues.has(publicValue) &&
4043
+ !pending.optionValues.has(publicValue)
4044
+ ) {
4045
+ if (
4046
+ publicValue.trim().length === 0 ||
4047
+ publicValue.includes("\0") ||
4048
+ utf8Bytes(publicValue) > MAX_FREEFORM_RESPONSE_BYTES
4049
+ ) {
4050
+ throw new OmpPublicError("OMP freeform response is invalid");
4051
+ }
4052
+ this.pendingFreeformSelection = {
4053
+ value: publicValue,
4054
+ nativeSelectId: pending.nativeId,
4055
+ generation: pending.generation,
4056
+ runtime: pending.runtime,
4057
+ ...(pending.turnId ? { turnId: pending.turnId } : {}),
4058
+ };
4059
+ return {
4060
+ type: "extension_ui_response",
4061
+ id: nativeId,
4062
+ value: pending.freeformSentinel,
4063
+ };
4064
+ }
4065
+ if (request.method === "select") {
4066
+ const mapped =
4067
+ pending.optionValues.get(publicValue) ?? pending.displayValues.get(publicValue);
4068
+ if (mapped === undefined) {
4069
+ throw new OmpPublicError("OMP selection response is invalid");
4070
+ }
4071
+ return { type: "extension_ui_response", id: nativeId, value: mapped };
4072
+ }
4073
+ return { type: "extension_ui_response", id: nativeId, value: publicValue };
4074
+ }
4075
+
4076
+ private resolvePermissionByNativeId(nativeId: string): void {
4077
+ for (const permissions of [this.pendingPermissions, this.inFlightPermissions]) {
4078
+ for (const [permissionId, pending] of permissions) {
4079
+ if (pending.nativeId !== nativeId) continue;
4080
+ permissions.delete(permissionId);
4081
+ if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
4082
+ this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
4083
+ this.reevaluateDeferredPermissionTerminal();
4084
+ return;
4085
+ }
4086
+ }
4087
+ }
4088
+
4089
+ private resolveTurnPermissions(turnId: string): void {
4090
+ if (this.pendingFreeformSelection?.turnId === turnId) this.pendingFreeformSelection = null;
4091
+ this.resolvePermissions((pending) => pending.turnId === turnId, true);
4092
+ this.resolveToolPermissions((pending) => pending.turnId === turnId, true);
4093
+ }
4094
+ private resolveAllPermissions(cancelNative = false): void {
4095
+ this.pendingFreeformSelection = null;
4096
+ this.resolvePermissions(() => true, cancelNative);
4097
+ this.resolveToolPermissions(() => true, cancelNative);
4098
+ }
4099
+
4100
+ private resolvePermissions(
4101
+ matches: (pending: PendingPermission) => boolean,
4102
+ cancelNative: boolean,
4103
+ ): void {
4104
+ const permissionIds = new Set<string>();
4105
+ for (const permissions of [this.pendingPermissions, this.inFlightPermissions]) {
4106
+ for (const [permissionId, pending] of permissions) {
4107
+ if (!matches(pending)) continue;
4108
+ permissions.delete(permissionId);
4109
+ if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
4110
+ permissionIds.add(permissionId);
4111
+ if (cancelNative && permissions === this.pendingPermissions) {
4112
+ void pending.runtime
4113
+ .respondToExtensionUi({
4114
+ type: "extension_ui_response",
4115
+ id: pending.nativeId,
4116
+ cancelled: true,
4117
+ })
4118
+ .catch(() => {
4119
+ if (!this.closed && !this.runtimeDead) {
4120
+ this.invalidateRuntime("OMP permission cancellation failed");
4121
+ }
4122
+ });
4123
+ }
4124
+ }
4125
+ }
4126
+ for (const permissionId of permissionIds) {
4127
+ this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
4128
+ }
4129
+ this.reevaluateDeferredPermissionTerminal();
4130
+ }
4131
+ private resolveToolPermissions(
4132
+ matches: (pending: PendingToolPermission) => boolean,
4133
+ cancelNative: boolean,
4134
+ ): void {
4135
+ const permissionIds = new Set<string>();
4136
+ for (const permissions of [this.pendingToolPermissions, this.inFlightToolPermissions]) {
4137
+ for (const [permissionId, pending] of permissions) {
4138
+ if (!matches(pending)) continue;
4139
+ permissions.delete(permissionId);
4140
+ if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
4141
+ this.resolvedToolApprovalIds.add(pending.nativeId);
4142
+ permissionIds.add(permissionId);
4143
+ if (cancelNative && permissions === this.pendingToolPermissions) {
4144
+ void pending.runtime
4145
+ .respondToToolApproval({
4146
+ type: "tool_approval_response",
4147
+ id: pending.nativeId,
4148
+ toolCallId: pending.toolCallId,
4149
+ cancelled: true,
4150
+ })
4151
+ .catch(() => {
4152
+ if (!this.closed && !this.runtimeDead) {
4153
+ this.invalidateRuntime("OMP tool permission cancellation failed");
4154
+ }
4155
+ });
4156
+ }
4157
+ }
4158
+ }
4159
+ for (const permissionId of permissionIds) {
4160
+ this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
4161
+ }
4162
+ this.reevaluateDeferredPermissionTerminal();
4163
+ }
4164
+
4165
+ private armToolPermissionTimeout(permissionId: string, pending: PendingToolPermission): void {
4166
+ if (pending.expiresAt === undefined) return;
4167
+ const remainingMs = Math.max(0, pending.expiresAt - Date.now());
4168
+ pending.timer = this.scheduler.set(() => {
4169
+ if (this.pendingToolPermissions.get(permissionId) !== pending) return;
4170
+ this.pendingToolPermissions.delete(permissionId);
4171
+ this.inFlightToolPermissions.set(permissionId, pending);
4172
+ void pending.runtime
4173
+ .respondToToolApproval({
4174
+ type: "tool_approval_response",
4175
+ id: pending.nativeId,
4176
+ toolCallId: pending.toolCallId,
4177
+ cancelled: true,
4178
+ timedOut: true,
4179
+ })
4180
+ .then(
4181
+ () => {
4182
+ if (this.inFlightToolPermissions.get(permissionId) !== pending) return;
4183
+ this.inFlightToolPermissions.delete(permissionId);
4184
+ this.resolvedToolApprovalIds.add(pending.nativeId);
4185
+ this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
4186
+ this.reevaluateDeferredPermissionTerminal();
4187
+ },
4188
+ () => this.handleRuntimeFailure(),
4189
+ );
4190
+ }, remainingMs);
4191
+ }
4192
+
4193
+ private armPermissionTimeout(permissionId: string, pending: PendingPermission): void {
4194
+ if (pending.expiresAt === undefined) return;
4195
+ const remainingMs = Math.max(0, pending.expiresAt - Date.now());
4196
+ pending.timer = this.scheduler.set(() => {
4197
+ if (this.pendingPermissions.get(permissionId) !== pending) return;
4198
+ this.pendingPermissions.delete(permissionId);
4199
+ if (!this.permissionOwnerIsCurrent(pending)) {
4200
+ this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
4201
+ this.reevaluateDeferredPermissionTerminal();
4202
+ return;
4203
+ }
4204
+ this.inFlightPermissions.set(permissionId, pending);
4205
+ void pending.runtime
4206
+ .respondToExtensionUi({
4207
+ type: "extension_ui_response",
4208
+ id: pending.nativeId,
4209
+ cancelled: true,
4210
+ timedOut: true,
4211
+ })
4212
+ .then(
4213
+ () => {
4214
+ if (this.inFlightPermissions.get(permissionId) !== pending) return;
4215
+ this.inFlightPermissions.delete(permissionId);
4216
+ this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
4217
+ this.reevaluateDeferredPermissionTerminal();
4218
+ },
4219
+ () => this.handleRuntimeFailure(),
4220
+ );
4221
+ }, remainingMs);
4222
+ }
4223
+
4224
+ private permissionOwnerIsCurrent(pending: PendingPermission | PendingToolPermission): boolean {
4225
+ if (
4226
+ this.closed ||
4227
+ this.runtimeDead ||
4228
+ pending.generation !== this.generation ||
4229
+ pending.runtime !== this.runtime
4230
+ ) {
4231
+ return false;
4232
+ }
4233
+ if (pending.turnId === undefined) return true;
4234
+ return this.activeTurn?.turnId === pending.turnId && !this.activeTurn.terminal;
4235
+ }
4236
+
4237
+ private reevaluateDeferredPermissionTerminal(): void {
4238
+ const turn = this.activeTurn;
4239
+ if (!turn?.deferredAgentEnd || turn.terminal || turn.terminalizing) return;
4240
+ const ownsTurn = (pending: PendingPermission | PendingToolPermission) =>
4241
+ pending.turnId === turn.turnId;
4242
+ if (
4243
+ [...this.pendingPermissions.values()].some(ownsTurn) ||
4244
+ [...this.inFlightPermissions.values()].some(ownsTurn) ||
4245
+ [...this.pendingToolPermissions.values()].some(ownsTurn) ||
4246
+ [...this.inFlightToolPermissions.values()].some(ownsTurn)
4247
+ ) {
4248
+ return;
4249
+ }
4250
+ const hasAssistantEvidence =
4251
+ turn.deferredAgentEnd.messages?.some((message) => message.role === "assistant") ?? false;
4252
+ if (!hasAssistantEvidence || turn.awaitingPermissionEvidence) return;
4253
+ const deferred = turn.deferredAgentEnd;
4254
+ turn.deferredAgentEnd = undefined;
4255
+ this.beginTerminalization(turn, deferred);
4256
+ }
4257
+ private async slashSteerUnavailable(commandName: string): Promise<boolean> {
4258
+ if (!this.commandDiscoveryAvailable || !this.slashCommands.has(commandName)) {
4259
+ try {
4260
+ this.replaceSlashCommands(await this.runtime.getAvailableCommands());
4261
+ } catch {
4262
+ this.commandDiscoveryAvailable = false;
4263
+ return true;
4264
+ }
4265
+ }
4266
+ return this.slashCommands.has(commandName);
4267
+ }
4268
+
4269
+ private publishCorrelatedUser(turn: ActiveTurn, pending: PendingUser, entryId?: string): void {
4270
+ if (entryId) {
4271
+ if (this.emittedEntryIds.has(entryId)) return;
4272
+ this.seenEntryIds.add(entryId);
4273
+ this.emittedEntryIds.add(entryId);
4274
+ if (!turn.terminalOwnershipRequired) this.markTerminalOwnershipEvidence(turn);
4275
+ if (this.branchWatermarkValid && !this.branchEntryIds.has(entryId)) {
4276
+ if (this.branchEntryIds.size >= MAX_UNCLAIMED_BRANCH_ENTRIES)
4277
+ this.quarantineBranchEntries();
4278
+ else this.branchEntryIds.add(entryId);
4279
+ }
4280
+ const unclaimedIndex = this.unclaimedBranchEntries.findIndex(
4281
+ (entry) => entry.entryId === entryId,
4282
+ );
4283
+ if (unclaimedIndex >= 0) this.unclaimedBranchEntries.splice(unclaimedIndex, 1);
4284
+ }
4285
+ this.projector.publishUser(pending.text, pending.clientMessageId, entryId);
4286
+ }
4287
+
4288
+ private markAgentEvidence(turn: ActiveTurn): void {
4289
+ turn.agentInvoked = true;
4290
+ turn.nativeActivity = true;
4291
+ turn.activitySequence += 1;
4292
+ turn.localOnlyEligible = false;
4293
+ this.cancelLocalOnlyCompletion(turn);
4294
+ if (!turn.awaitingPermissionEvidence) turn.deferredAgentEnd = undefined;
4295
+ }
4296
+
4297
+ private markTerminalOwnershipEvidence(turn: ActiveTurn): void {
4298
+ turn.terminalOwnershipEvidence = true;
4299
+ this.cancelTerminalOwnershipTimeout(turn);
4300
+ }
4301
+
4302
+ private scheduleLocalOnlyCompletion(turn: ActiveTurn): void {
4303
+ if (
4304
+ turn.agentInvoked !== false ||
4305
+ !turn.localOnlyEligible ||
4306
+ turn.awaitingPermissionEvidence ||
4307
+ turn.localOnlyDisabled ||
4308
+ turn.steersInFlight > 0
4309
+ ) {
4310
+ return;
4311
+ }
4312
+ this.cancelLocalOnlyCompletion(turn);
4313
+ turn.localOnlyTimer = this.scheduler.set(() => {
4314
+ turn.localOnlyTimer = undefined;
4315
+ return this.completeLocalOnlyTurn(turn);
4316
+ }, LOCAL_ONLY_SETTLE_MS);
4317
+ }
4318
+
4319
+ private cancelLocalOnlyCompletion(turn: ActiveTurn): void {
4320
+ if (turn.localOnlyTimer === undefined) return;
4321
+ this.scheduler.clear(turn.localOnlyTimer);
4322
+ turn.localOnlyTimer = undefined;
4323
+ }
4324
+
4325
+ private scheduleTerminalOwnershipTimeout(turn: ActiveTurn): void {
4326
+ if (
4327
+ turn.terminalOwnershipTimer !== undefined ||
4328
+ turn.terminalOwnershipEvidence ||
4329
+ (turn.agentInvoked === false && turn.localOnlyEligible)
4330
+ ) {
4331
+ return;
4332
+ }
4333
+ turn.terminalOwnershipTimer = this.scheduler.set(() => {
4334
+ turn.terminalOwnershipTimer = undefined;
4335
+ if (
4336
+ this.closed ||
4337
+ turn.terminal ||
4338
+ this.activeTurn !== turn ||
4339
+ turn.terminalOwnershipEvidence
4340
+ ) {
4341
+ return;
4342
+ }
4343
+ this.handleRuntimeFailure("OMP terminal ownership could not be confirmed");
4344
+ }, AGENT_END_STATE_TIMEOUT_MS);
4345
+ }
4346
+
4347
+ private cancelTerminalOwnershipTimeout(turn: ActiveTurn): void {
4348
+ if (turn.terminalOwnershipTimer === undefined) return;
4349
+ this.scheduler.clear(turn.terminalOwnershipTimer);
4350
+ turn.terminalOwnershipTimer = undefined;
4351
+ }
4352
+
4353
+ private async completeLocalOnlyTurn(turn: ActiveTurn): Promise<void> {
4354
+ await Promise.allSettled(turn.userLookups);
4355
+ if (this.closed || turn.terminal || this.activeTurn !== turn) return;
4356
+ if (
4357
+ turn.terminal ||
4358
+ turn.agentInvoked !== false ||
4359
+ !turn.localOnlyEligible ||
4360
+ turn.awaitingPermissionEvidence ||
4361
+ turn.nativeActivity ||
4362
+ turn.localOnlyDisabled ||
4363
+ turn.steersInFlight > 0 ||
4364
+ this.activeTurn !== turn
4365
+ ) {
4366
+ return;
4367
+ }
4368
+ turn.usageSampleFloor = this.usageSequence + 1;
4369
+ if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
4370
+ this.usageSample = null;
4371
+ }
4372
+ this.publishPendingUsers(turn);
4373
+ await this.finishTurn(turn, "completed", undefined, false, false, true);
4374
+ }
4375
+
4376
+ private beginTerminalization(
4377
+ turn: ActiveTurn,
4378
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
4379
+ ): void {
4380
+ if (turn.terminal || turn.terminalizing || turn.agentEndPending || this.activeTurn !== turn) {
4381
+ return;
4382
+ }
4383
+ if (!turn.interrupted && this.deferAgentEndForSubsessions(turn, event)) return;
4384
+ turn.agentEndPending = true;
4385
+ turn.usageSampleFloor = this.usageSequence + 1;
4386
+ if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
4387
+ this.usageSample = null;
4388
+ }
4389
+ this.stopUsagePoll(turn);
4390
+ turn.agentEndDeadlineTimer = this.scheduler.set(() => {
4391
+ turn.agentEndDeadlineTimer = undefined;
4392
+ if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
4393
+ if (
4394
+ turn.userEchoObserved ||
4395
+ (turn.terminalOwnershipRequired && !turn.terminalOwnershipEvidence)
4396
+ ) {
4397
+ this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
4398
+ } else {
4399
+ void this.completeAgentEnd(turn, event);
4400
+ }
4401
+ }, AGENT_END_SETTLE_MS);
4402
+ this.finishFromAgentEnd(turn, event);
4403
+ }
4404
+
4405
+ private resumeAfterFailedSteer(turn: ActiveTurn): void {
4406
+ if (turn.terminal || this.activeTurn !== turn || turn.steersInFlight > 0) return;
4407
+ const deferred = turn.deferredAgentEnd;
4408
+ if (deferred) {
4409
+ turn.deferredAgentEnd = undefined;
4410
+ this.beginTerminalization(turn, deferred);
4411
+ return;
4412
+ }
4413
+ if (turn.localOnlyEligible && !turn.localOnlyDisabled && !turn.nativeActivity) {
4414
+ this.scheduleLocalOnlyCompletion(turn);
4415
+ }
4416
+ }
4417
+ private deferAgentEndForSubsessions(
4418
+ turn: ActiveTurn,
4419
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
4420
+ ): boolean {
4421
+ if (!this.subsessions?.hasActiveChildren()) return false;
4422
+ turn.terminalizing = false;
4423
+ turn.deferredAgentEnd = event;
4424
+ void this.subsessions.reconcile(this.runtime).catch(() => {
4425
+ if (!turn.terminal && this.activeTurn === turn) {
4426
+ this.handleRuntimeFailure("OMP subagent reconciliation failed");
4427
+ }
4428
+ });
4429
+ return true;
4430
+ }
4431
+
4432
+ private resumeDeferredAgentEnd(): void {
4433
+ const turn = this.activeTurn;
4434
+ if (
4435
+ !turn ||
4436
+ turn.terminal ||
4437
+ turn.terminalizing ||
4438
+ turn.steersInFlight > 0 ||
4439
+ this.subsessions?.hasActiveChildren()
4440
+ ) {
4441
+ return;
4442
+ }
4443
+ const event = turn.deferredAgentEnd;
4444
+ if (!event) return;
4445
+ turn.deferredAgentEnd = undefined;
4446
+ this.beginTerminalization(turn, event);
4447
+ }
4448
+
4449
+ private finishFromAgentEnd(
4450
+ turn: ActiveTurn,
4451
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
4452
+ ): void {
4453
+ if (!turn.agentEndPending || turn.terminal) return;
4454
+ if (turn.agentEndCheck) {
4455
+ turn.deferredAgentEnd = event;
4456
+ return;
4457
+ }
4458
+ const check = this.checkAgentEndState(turn, event);
4459
+ turn.agentEndCheck = check;
4460
+ void check.finally(() => {
4461
+ if (turn.agentEndCheck !== check) return;
4462
+ turn.agentEndCheck = undefined;
4463
+ const deferred = turn.deferredAgentEnd;
4464
+ if (!deferred || !turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
4465
+ turn.deferredAgentEnd = undefined;
4466
+ this.finishFromAgentEnd(turn, deferred);
4467
+ });
4468
+ }
4469
+
4470
+ private async checkAgentEndState(
4471
+ turn: ActiveTurn,
4472
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
4473
+ ): Promise<void> {
4474
+ // Evidence arriving during this check cannot authorize an older terminal frame.
4475
+ const ownershipObserved =
4476
+ turn.terminalOwnershipEvidence || turn.bufferedTerminalOwnershipEvidence;
4477
+ await Promise.allSettled(turn.userLookups);
4478
+ if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
4479
+ while (turn.userEchoes.length > 0) {
4480
+ this.drainUserEchoes(turn);
4481
+ if (turn.userLookups.size === 0) break;
4482
+ await Promise.allSettled(turn.userLookups);
4483
+ if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
4484
+ }
4485
+ if (turn.interrupted) {
4486
+ await this.completeAgentEnd(turn, event);
4487
+ return;
4488
+ }
4489
+ const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4490
+ if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
4491
+ if (!turn.interrupted && this.subsessions?.hasActiveChildren()) {
4492
+ turn.agentEndPending = false;
4493
+ if (this.deferAgentEndForSubsessions(turn, event)) return;
4494
+ }
4495
+ if (state) {
4496
+ if (state.isStreaming || state.isCompacting) {
4497
+ if (!turn.terminalOwnershipEvidence && !turn.terminalOwnershipRequired) {
4498
+ const message = "OMP agent_end arrived while the native runtime remained active";
4499
+ this.invalidateRuntime(message);
4500
+ await this.finishTurn(turn, "failed", { message }, true, true);
4501
+ return;
4502
+ }
4503
+ turn.agentEndPending = false;
4504
+ turn.terminalizing = false;
4505
+ turn.deferredAgentEnd = undefined;
4506
+ return;
4507
+ }
4508
+ if (
4509
+ turn.terminalOwnershipRequired &&
4510
+ (!ownershipObserved || !turn.terminalOwnershipEvidence)
4511
+ ) {
4512
+ turn.agentEndPending = false;
4513
+ turn.terminalizing = false;
4514
+ turn.deferredAgentEnd = undefined;
4515
+ this.scheduleTerminalOwnershipTimeout(turn);
4516
+ return;
4517
+ }
4518
+ await this.completeAgentEnd(turn, event, true);
4519
+ return;
4520
+ }
4521
+ if (turn.userEchoObserved) {
4522
+ this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
4523
+ return;
4524
+ }
4525
+ if (turn.agentEndRetryTimer === undefined) {
4526
+ turn.agentEndRetryTimer = this.scheduler.set(() => {
4527
+ turn.agentEndRetryTimer = undefined;
4528
+ this.finishFromAgentEnd(turn, event);
4529
+ }, USAGE_POLL_MS);
4530
+ }
4531
+ }
4532
+
4533
+ private async completeAgentEnd(
4534
+ turn: ActiveTurn,
4535
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
4536
+ providerIdle = false,
4537
+ ): Promise<void> {
4538
+ if (turn.terminal || this.activeTurn !== turn) return;
4539
+ let outcome = turn.interrupted ? ("canceled" as const) : terminalOutcome(event, turn);
4540
+ if (!outcome && providerIdle && event.messageCount !== undefined) {
4541
+ const runtime = this.runtime;
4542
+ const history = await this.readRuntimeHistoryWithTimeout(runtime);
4543
+ if (
4544
+ history &&
4545
+ history.length <= MAX_REPLAY_MESSAGES &&
4546
+ this.isCurrentRuntime(runtime, turn.generation) &&
4547
+ !turn.terminal &&
4548
+ !turn.interrupted &&
4549
+ this.activeTurn === turn
4550
+ ) {
4551
+ const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4552
+ if (
4553
+ turn.terminal ||
4554
+ this.activeTurn !== turn ||
4555
+ !this.isCurrentRuntime(runtime, turn.generation)
4556
+ )
4557
+ return;
4558
+ if (!turn.interrupted && state && (state.isStreaming || state.isCompacting)) {
4559
+ turn.agentEndPending = false;
4560
+ turn.deferredAgentEnd = undefined;
4561
+ return;
4562
+ }
4563
+ if (state && !state.isStreaming && !state.isCompacting) {
4564
+ outcome = historyTerminalOutcome(history, event.messageCount, turn, event.messages ?? []);
4565
+ }
4566
+ }
4567
+ }
4568
+ if (turn.terminal || this.activeTurn !== turn) return;
4569
+ if (turn.interrupted || outcome === "canceled") {
4570
+ this.subsessions?.terminalize("canceled");
4571
+ await this.finishTurn(turn, "canceled");
4572
+ return;
4573
+ }
4574
+ if (outcome === "completed") {
4575
+ await this.finishTurn(turn, "completed");
4576
+ return;
4577
+ }
4578
+ const error =
4579
+ outcome === "failed" ? "OMP assistant turn failed" : unknownTerminalOutcomeError(event, turn);
4580
+ this.subsessions?.terminalize("failed");
4581
+ await this.finishTurn(turn, "failed", { message: error });
4582
+ }
4583
+ private publishPendingUsers(turn: ActiveTurn): void {
4584
+ for (const pending of turn.pendingUsers.splice(0)) {
4585
+ for (const echo of pending.bufferedEchoes) {
4586
+ const entryId = nativeEntryId(echo);
4587
+ if (entryId) this.seenEntryIds.add(entryId);
4588
+ }
4589
+ if (pending.accepted && pending.fallbackOnFinish) {
4590
+ this.quarantineBranchEntries();
4591
+ this.projector.publishUser(pending.text, pending.clientMessageId);
4592
+ }
4593
+ }
4594
+ for (const echo of turn.userEchoes) {
4595
+ const entryId = nativeEntryId(echo);
4596
+ if (entryId) this.seenEntryIds.add(entryId);
4597
+ }
4598
+ turn.userEchoes.length = 0;
4599
+ }
4600
+
4601
+ private acceptPendingUser(turn: ActiveTurn, pending: PendingUser): void {
4602
+ pending.accepted = true;
4603
+ pending.fallbackOnFinish = true;
4604
+ if (pending.bufferedEchoes.length > 0) {
4605
+ turn.userEchoes.unshift(...pending.bufferedEchoes.splice(0));
4606
+ }
4607
+ this.drainUserEchoes(turn);
4608
+ }
4609
+
4610
+ private removePendingUser(turn: ActiveTurn, pending: PendingUser): void {
4611
+ const index = turn.pendingUsers.indexOf(pending);
4612
+ if (index >= 0) turn.pendingUsers.splice(index, 1);
4613
+ for (const echo of pending.bufferedEchoes) {
4614
+ const entryId = nativeEntryId(echo);
4615
+ if (entryId) this.seenEntryIds.add(entryId);
4616
+ }
4617
+ pending.bufferedEchoes.length = 0;
4618
+ }
4619
+
4620
+ private settleUnstartedTurn(turn: ActiveTurn): void {
4621
+ if (turn.started || turn.terminal) return;
4622
+ turn.steerReady.resolve();
4623
+ turn.starting = false;
4624
+ turn.terminal = true;
4625
+ this.stopUsagePoll(turn);
4626
+ this.resolveTurnPermissions(turn.turnId);
4627
+ this.projector.finishTurn(turn.turnId);
4628
+ if (this.activeTurn === turn) this.activeTurn = null;
4629
+ }
4630
+
4631
+ private publishPromptResult(
4632
+ turn: ActiveTurn,
4633
+ result: Extract<ProviderEvent, { type: "session.prompt_result" }>["result"],
4634
+ ): void {
4635
+ if (turn.promptResultEmitted) return;
4636
+ turn.promptResultEmitted = true;
4637
+ this.emit({
4638
+ type: "session.prompt_result",
4639
+ sessionId: this.id,
4640
+ clientMessageId: turn.clientMessageId,
4641
+ result,
4642
+ });
4643
+ }
4644
+
4645
+ private startTurn(turn: ActiveTurn, pollUsage = true): void {
4646
+ if (turn.started || turn.terminal) return;
4647
+ turn.started = true;
4648
+ turn.starting = false;
4649
+ turn.steerReady.resolve();
4650
+ this.emit({ type: "session.turn", sessionId: this.id, turnId: turn.turnId, state: "started" });
4651
+ if (pollUsage) this.pollUsage(turn);
4652
+ }
4653
+ private finishTurn(
4654
+ turn: ActiveTurn,
4655
+ state: "completed" | "failed" | "canceled",
4656
+ error?: ProviderError,
4657
+ usageSampled = false,
4658
+ override = false,
4659
+ preserveCompactions = false,
4660
+ ): Promise<void> {
4661
+ turn.steerReady.resolve();
4662
+ if (turn.terminal) return Promise.resolve();
4663
+ const current = turn.terminalOutcome;
4664
+ if (
4665
+ !current ||
4666
+ override ||
4667
+ state === "failed" ||
4668
+ (state === "canceled" && current.state === "completed")
4669
+ ) {
4670
+ turn.terminalOutcome = { state, ...(error ? { error } : {}), usageSampled };
4671
+ }
4672
+ if (turn.terminalization) {
4673
+ if (override) turn.terminalWake?.resolve();
4674
+ return turn.terminalization;
4675
+ }
4676
+ turn.terminalizing = true;
4677
+ turn.manualCompactionPending = false;
4678
+ turn.agentEndPending = false;
4679
+ this.cancelLocalOnlyCompletion(turn);
4680
+ this.cancelTerminalOwnershipTimeout(turn);
4681
+ this.stopUsagePoll(turn);
4682
+ if (turn.manualCompactionDeadlineTimer !== undefined) {
4683
+ this.scheduler.clear(turn.manualCompactionDeadlineTimer);
4684
+ turn.manualCompactionDeadlineTimer = undefined;
4685
+ }
4686
+ if (turn.agentEndRetryTimer !== undefined) {
4687
+ this.scheduler.clear(turn.agentEndRetryTimer);
4688
+ turn.agentEndRetryTimer = undefined;
4689
+ }
4690
+ if (turn.agentEndDeadlineTimer !== undefined) {
4691
+ this.scheduler.clear(turn.agentEndDeadlineTimer);
4692
+ turn.agentEndDeadlineTimer = undefined;
4693
+ }
4694
+ const wake = Promise.withResolvers<void>();
4695
+ turn.terminalWake = wake;
4696
+ const generation = turn.generation;
4697
+ const terminalization = (async () => {
4698
+ if (!turn.terminalOutcome?.usageSampled && !this.closed && !this.runtimeDead) {
4699
+ await Promise.race([
4700
+ this.boundedUsageSnapshot(turn, FINAL_USAGE_WAIT_MS),
4701
+ wake.promise.then(() => undefined),
4702
+ ]);
4703
+ }
4704
+ if (turn.terminal || this.activeTurn !== turn) return;
4705
+ let outcome = turn.terminalOutcome;
4706
+ if (!outcome) return;
4707
+ if (this.closed && outcome.state === "completed") {
4708
+ outcome = { state: "canceled", usageSampled: true };
4709
+ } else if (
4710
+ (this.runtimeDead || generation !== this.generation) &&
4711
+ outcome.state === "completed"
4712
+ ) {
4713
+ outcome = {
4714
+ state: "failed",
4715
+ error: { message: "OMP runtime failed" },
4716
+ usageSampled: true,
4717
+ };
4718
+ }
4719
+ this.imageMaterializer.clear();
4720
+ turn.terminal = true;
4721
+ if (this.usageSample?.turn === turn) this.usageSample = null;
4722
+ if (!preserveCompactions && this.activeCompaction) {
4723
+ if (outcome.state === "canceled") this.finishCompaction("canceled");
4724
+ else {
4725
+ this.finishCompaction("failed", {
4726
+ message: outcome.error?.message ?? "OMP compaction ended without a terminal result",
4727
+ });
4728
+ }
4729
+ }
4730
+ this.publishPendingUsers(turn);
4731
+ this.resolveTurnPermissions(turn.turnId);
4732
+ this.projector.finishTurn(turn.turnId, preserveCompactions);
4733
+ this.unclaimedBranchEntries.length = 0;
4734
+ this.emit({
4735
+ type: "session.turn",
4736
+ sessionId: this.id,
4737
+ turnId: turn.turnId,
4738
+ state: outcome.state,
4739
+ ...(outcome.error ? { error: outcome.error } : {}),
4740
+ });
4741
+ this.runtimeTurnCompleted = true;
4742
+ if (this.activeTurn === turn) this.activeTurn = null;
4743
+ })();
4744
+ turn.terminalization = terminalization;
4745
+ return terminalization;
4746
+ }
4747
+
4748
+ private invalidateRuntime(
4749
+ message: string,
4750
+ compactionState: "failed" | "canceled" = "failed",
4751
+ ): void {
4752
+ this.imageMaterializer.clear();
4753
+ if (this.closed || this.runtimeDead) return;
4754
+ this.discardedCompactionEnds = 0;
4755
+ this.resolveAllPermissions();
4756
+ this.recoveryUsesNativeConfig ||=
4757
+ this.configRefreshInFlight !== null || this.configRefreshDirty || this.configMutationInFlight;
4758
+ const turn = this.activeTurn;
4759
+ if (turn) this.stopUsagePoll(turn);
4760
+ this.usageSample = null;
4761
+ this.finishCompaction(compactionState, { message });
4762
+ this.lastUsage = null;
4763
+ this.generation += 1;
4764
+ this.projector.resetRuntimeGeneration("OMP runtime ended during compaction");
4765
+ this.runtimeDead = message;
4766
+ this.configRefreshAttempts = 0;
4767
+ this.configRefreshDirty = false;
4768
+ const configRefresh = this.configRefreshInFlight;
4769
+ this.cancelConfigRefreshRetry();
4770
+ this.unsubscribe();
4771
+ this.hostTools.detach();
4772
+ this.unsubscribe = () => {};
4773
+ const runtimeDisposal = this.runtimeDisposal ?? this.runtime.close();
4774
+ this.runtimeDisposal = configRefresh
4775
+ ? Promise.all([runtimeDisposal, configRefresh]).then(() => undefined)
4776
+ : runtimeDisposal;
4777
+ void this.runtimeDisposal.catch(() => undefined);
4778
+ }
4779
+
4780
+ private handleRuntimeFailure(message = "OMP runtime failed"): void {
4781
+ if (this.closed || this.runtimeDead) return;
4782
+ this.invalidateRuntime(message);
4783
+ this.subsessions?.terminalize("failed");
4784
+ const turn = this.activeTurn;
4785
+ if (!turn) return;
4786
+ this.publishPendingUsers(turn);
4787
+ this.publishPromptResult(turn, { type: "failed", error: { message } });
4788
+ if (turn.started) void this.finishTurn(turn, "failed", { message }, true, true);
4789
+ else {
4790
+ turn.steerReady.resolve();
4791
+ turn.terminal = true;
4792
+ this.projector.finishTurn(turn.turnId);
4793
+ if (this.activeTurn === turn) this.activeTurn = null;
4794
+ }
4795
+ }
4796
+ }