@omercnet/paseo-omp 0.2.1 → 0.3.0

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 (63) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +25 -13
  3. package/SUPPORT.md +6 -2
  4. package/TESTING.md +21 -18
  5. package/client/composer-pill-settings.tsx +157 -0
  6. package/client/external-url.ts +15 -0
  7. package/client/mcp-authorization.tsx +169 -0
  8. package/client/mcp-popover.tsx +155 -0
  9. package/client/memory-panel.tsx +8 -3
  10. package/client/memory-popover.tsx +8 -4
  11. package/client/omp-config-surface.tsx +189 -29
  12. package/client/omp-plugin-manager.tsx +302 -131
  13. package/client/omp-store-picker.tsx +89 -0
  14. package/client/omp-store-state.ts +45 -0
  15. package/client/paseo-types.ts +9 -0
  16. package/client/provider-diagnostics-state.ts +18 -7
  17. package/client/quota-popover.tsx +8 -3
  18. package/client/quota-state.ts +16 -7
  19. package/client/sessions-popover.tsx +8 -3
  20. package/docs/alpha-release-checklist.md +6 -8
  21. package/docs/configuration.md +8 -4
  22. package/docs/core-provider-issue-audit.md +3 -2
  23. package/docs/images/mcp-authorization-compact.png +0 -0
  24. package/docs/images/mcp-controls-wide.png +0 -0
  25. package/docs/images/plugin-manager.png +0 -0
  26. package/docs/images/workspace-settings.png +0 -0
  27. package/docs/installation.md +35 -19
  28. package/index.client.tsx +339 -123
  29. package/index.server.ts +44 -14
  30. package/package.json +7 -8
  31. package/paseo-plugin.json +2 -2
  32. package/scripts/prepare-dependencies.mjs +24 -0
  33. package/server/mcp-browser.ts +95 -0
  34. package/server/memory.ts +2 -2
  35. package/server/omp-config.ts +16 -7
  36. package/server/omp-plugins.ts +70 -21
  37. package/server/omp-settings.ts +232 -24
  38. package/server/paths.ts +128 -11
  39. package/server/provider/catalog.ts +3 -4
  40. package/server/provider/connection.ts +213 -9
  41. package/server/provider/host-tools.ts +71 -0
  42. package/server/provider/omp-rpc.ts +82 -15
  43. package/server/provider/profile-providers.ts +249 -0
  44. package/server/provider/registration.ts +11 -0
  45. package/server/provider/session-descriptors.ts +306 -1
  46. package/server/provider/session.ts +704 -249
  47. package/server/provider/subsessions.ts +4 -1
  48. package/server/provider/timeline-projector.ts +70 -33
  49. package/server/provider-diagnostics.ts +122 -36
  50. package/server/quota.ts +3 -2
  51. package/server/sessions.ts +2 -2
  52. package/shared/composer-pill-settings.ts +28 -0
  53. package/shared/external-url.ts +21 -0
  54. package/shared/hub.ts +3 -3
  55. package/shared/mcp.ts +47 -0
  56. package/shared/memory.ts +2 -1
  57. package/shared/omp-config.ts +5 -1
  58. package/shared/omp-plugins.ts +74 -33
  59. package/shared/omp-settings.ts +8 -1
  60. package/shared/omp-store.ts +58 -0
  61. package/shared/provider-diagnostics.ts +12 -3
  62. package/shared/quota.ts +2 -1
  63. package/shared/sessions.ts +2 -1
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import type {
3
3
  ProviderConfigState,
4
4
  ProviderContent,
5
+ ProviderError,
5
6
  ProviderEvent,
6
7
  ProviderInput,
7
8
  ProviderPermissionResponse,
@@ -72,6 +73,7 @@ type OmpQuestionRequest = Extract<
72
73
  type Emit = (event: ProviderEvent) => void;
73
74
  const LOCAL_ONLY_SETTLE_MS = 5_000;
74
75
  const AGENT_END_STATE_TIMEOUT_MS = 2_000;
76
+ const AGENT_END_HISTORY_TIMEOUT_MS = 2_000;
75
77
  const CONFIG_REFRESH_RETRY_BASE_MS = 250;
76
78
  const CONFIG_REFRESH_MAX_ATTEMPTS = 3;
77
79
  const USAGE_POLL_MS = 1_000;
@@ -82,6 +84,7 @@ const AGENT_END_SETTLE_MS = 5_000;
82
84
  const MAX_PROMPT_PARTS = 64;
83
85
  const MAX_PROMPT_TEXT_LENGTH = 1024 * 1024;
84
86
  const RPC_REQUEST_ID_BYTES = 36;
87
+ const MAX_AGENT_END_CORRELATION_MESSAGES = 512;
85
88
  const MAX_TRACKED_ENTRY_IDS = 1_024;
86
89
  const MAX_UNCLAIMED_BRANCH_ENTRIES = 1_024;
87
90
  const MAX_PENDING_USERS = 256;
@@ -140,6 +143,16 @@ function applicableThinkingLevel(
140
143
  return model?.reasoning === false ? undefined : level;
141
144
  }
142
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
+
143
156
  export function ompPersistenceSessionId(input: SessionOpenInput): string | undefined {
144
157
  if (!input.persistence) return;
145
158
  if (input.persistence.version !== 1) {
@@ -215,6 +228,24 @@ type PendingUser = {
215
228
  fallbackOnFinish: boolean;
216
229
  bufferedEchoes: OmpMessage[];
217
230
  };
231
+ type TerminalCorrelationEvidence = "fresh-native-user" | "current-assistant";
232
+
233
+ type TerminalCorrelation = {
234
+ policy: "initial-turn" | "ordered-legacy" | "native-command";
235
+ evidence: Map<TerminalCorrelationEvidence, number>;
236
+ };
237
+
238
+ type TerminalCandidate = {
239
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>;
240
+ arrivalSequence: number;
241
+ confidence:
242
+ | "keyed"
243
+ | "initial-turn"
244
+ | "ordered-legacy"
245
+ | "native-command"
246
+ | "interrupted"
247
+ | "ambiguous";
248
+ };
218
249
 
219
250
  type ActiveTurn = {
220
251
  turnId: string;
@@ -232,10 +263,8 @@ type ActiveTurn = {
232
263
  awaitingPermissionEvidence: boolean;
233
264
  activitySequence: number;
234
265
  acknowledged: boolean;
235
- terminalOwnershipEvidence: boolean;
236
- terminalOwnershipRequired: boolean;
266
+ terminalCorrelation: TerminalCorrelation;
237
267
  replayingBufferedEvents: boolean;
238
- bufferedTerminalOwnershipEvidence: boolean;
239
268
  agentInvoked?: boolean;
240
269
  nativeRequestId?: string;
241
270
  promptAcceptedEventIndex?: number;
@@ -250,21 +279,25 @@ type ActiveTurn = {
250
279
  agentEndRetryTimer?: unknown;
251
280
  agentEndDeadlineTimer?: unknown;
252
281
  agentEndCheck?: Promise<void>;
253
- terminalOwnershipTimer?: unknown;
282
+ ambiguousTerminalTimer?: unknown;
254
283
  terminalizing: boolean;
255
284
  terminalization?: Promise<void>;
256
285
  terminalOutcome?: TurnOutcome;
257
286
  terminalWake?: VoidDeferred;
258
287
  steerReady: VoidDeferred;
259
288
  steersInFlight: number;
260
- deferredAgentEnd?: Extract<OmpRpcEvent, { type: "agent_end" }>;
289
+ deferredAgentEnd?: TerminalCandidate;
290
+ activeToolCallIds: Set<string>;
261
291
  bufferedEvents: OmpRpcEvent[];
262
292
  pendingUsers: PendingUser[];
263
293
  userEchoes: OmpMessage[];
264
294
  userCorrelationActive: boolean;
265
295
  userLookups: Set<Promise<void>>;
266
296
  completedMessageCount: number;
267
- lastCompletedAssistantFailed?: boolean;
297
+ streamedMessageEntryIds: string[];
298
+ streamedMessageIdentityComplete: boolean;
299
+ lastCompletedAssistantOutcome?: AgentEndOutcome;
300
+ lastCompletedAssistantEntryId?: string;
268
301
  };
269
302
 
270
303
  type PendingAbort = {
@@ -322,7 +355,7 @@ type VoidDeferred = {
322
355
 
323
356
  type TurnOutcome = {
324
357
  state: "completed" | "failed" | "canceled";
325
- error?: { message: string };
358
+ error?: ProviderError;
326
359
  usageSampled: boolean;
327
360
  };
328
361
 
@@ -598,30 +631,130 @@ function nativeEntryId(message: OmpMessage): string | undefined {
598
631
  return message.entryId;
599
632
  }
600
633
 
601
- function terminalError(
634
+ type AgentEndOutcome = "completed" | "failed" | "canceled";
635
+ type AssistantTerminalStatus = AgentEndOutcome | "unavailable";
636
+
637
+ function assistantTerminalOutcome(
638
+ message: Extract<OmpMessage, { role: "assistant" }>,
639
+ ): AgentEndOutcome {
640
+ const stopReason = message.stopReason?.toLowerCase();
641
+ if (stopReason === "aborted" || stopReason === "canceled" || stopReason === "cancelled") {
642
+ return "canceled";
643
+ }
644
+ return stopReason === "error" || message.errorMessage ? "failed" : "completed";
645
+ }
646
+
647
+ function lastAssistantStatus(
648
+ messages: readonly OmpMessage[],
649
+ startIndex = 0,
650
+ ): AssistantTerminalStatus {
651
+ for (let index = messages.length - 1; index >= startIndex; index -= 1) {
652
+ const message = messages[index];
653
+ if (message?.role !== "assistant") continue;
654
+ return assistantTerminalOutcome(message);
655
+ }
656
+ return "unavailable";
657
+ }
658
+
659
+ function terminalOutcome(
602
660
  event: Extract<OmpRpcEvent, { type: "agent_end" }>,
603
- turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantFailed">,
604
- ): string | undefined {
661
+ turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
662
+ ): AgentEndOutcome | undefined {
605
663
  const messages = event.messages;
606
- if (messages) {
607
- for (let index = messages.length - 1; index >= 0; index -= 1) {
608
- const message = messages[index];
609
- if (message?.role !== "assistant") continue;
610
- return message.stopReason === "error" || message.errorMessage
611
- ? "OMP assistant turn failed"
612
- : undefined;
613
- }
664
+ if (
665
+ messages !== undefined &&
666
+ (event.messageCount === undefined || messages.length >= event.messageCount)
667
+ ) {
668
+ const status = lastAssistantStatus(messages);
669
+ return status === "unavailable" ? "completed" : status;
670
+ }
671
+ if (event.messageCount === 0) return "completed";
672
+ if (
673
+ turn.lastCompletedAssistantOutcome !== undefined &&
674
+ (event.messageCount === undefined || turn.completedMessageCount >= event.messageCount)
675
+ ) {
676
+ return turn.lastCompletedAssistantOutcome;
614
677
  }
615
- if (messages && event.messageCount === undefined) return undefined;
616
- if (event.messageCount === 0) return undefined;
678
+ return undefined;
679
+ }
680
+
681
+ function historyTerminalOutcome(
682
+ messages: readonly OmpMessage[],
683
+ declaredCount: number,
684
+ turn: Pick<
685
+ ActiveTurn,
686
+ | "streamedMessageEntryIds"
687
+ | "streamedMessageIdentityComplete"
688
+ | "lastCompletedAssistantOutcome"
689
+ | "lastCompletedAssistantEntryId"
690
+ >,
691
+ retainedMessages: readonly OmpMessage[],
692
+ ): AgentEndOutcome | undefined {
617
693
  if (
618
- turn.lastCompletedAssistantFailed !== undefined &&
619
- (event.messageCount === undefined ||
620
- turn.completedMessageCount + (messages?.length ?? 0) >= event.messageCount)
694
+ messages.length < declaredCount ||
695
+ !turn.streamedMessageIdentityComplete ||
696
+ turn.streamedMessageEntryIds.length === 0
621
697
  ) {
622
- return turn.lastCompletedAssistantFailed ? "OMP assistant turn failed" : undefined;
698
+ return undefined;
699
+ }
700
+ const startIndex = messages.length - declaredCount;
701
+ let historyIndex = startIndex;
702
+ for (const entryId of turn.streamedMessageEntryIds) {
703
+ while (historyIndex < messages.length) {
704
+ const historyMessage = messages[historyIndex];
705
+ if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
706
+ historyIndex += 1;
707
+ }
708
+ if (historyIndex >= messages.length) return undefined;
709
+ const correlated = messages[historyIndex];
710
+ if (
711
+ entryId === turn.lastCompletedAssistantEntryId &&
712
+ (correlated?.role !== "assistant" ||
713
+ assistantTerminalOutcome(correlated) !== turn.lastCompletedAssistantOutcome)
714
+ )
715
+ return undefined;
716
+ historyIndex += 1;
623
717
  }
624
- return "OMP agent_end omitted terminal messages; outcome is unknown";
718
+ historyIndex = startIndex;
719
+ for (const retained of retainedMessages) {
720
+ const entryId = nativeEntryId(retained);
721
+ if (!entryId) return undefined;
722
+ while (historyIndex < messages.length) {
723
+ const historyMessage = messages[historyIndex];
724
+ if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
725
+ historyIndex += 1;
726
+ }
727
+ if (historyIndex >= messages.length) return undefined;
728
+ const correlated = messages[historyIndex];
729
+ if (!correlated || correlated.role !== retained.role) return undefined;
730
+ if (
731
+ retained.role === "assistant" &&
732
+ correlated.role === "assistant" &&
733
+ assistantTerminalOutcome(retained) !== assistantTerminalOutcome(correlated)
734
+ )
735
+ return undefined;
736
+ historyIndex += 1;
737
+ }
738
+ const status = lastAssistantStatus(messages, startIndex);
739
+ return status === "unavailable" ? undefined : status;
740
+ }
741
+
742
+ function unknownTerminalOutcomeError(
743
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
744
+ turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
745
+ ): string {
746
+ const retainedMessages = event.messages?.length ?? 0;
747
+ const retainedStatus = event.messages ? lastAssistantStatus(event.messages) : "unavailable";
748
+ const lastStatus =
749
+ retainedStatus === "unavailable"
750
+ ? (turn.lastCompletedAssistantOutcome ?? "unavailable")
751
+ : retainedStatus;
752
+ return (
753
+ "OMP agent_end omitted terminal messages; outcome is unknown " +
754
+ `(declaredCount=${event.messageCount ?? "unavailable"}, ` +
755
+ `observedCount=${turn.completedMessageCount}, ` +
756
+ `retainedTerminalMessages=${retainedMessages}, lastAssistantStatus=${lastStatus})`
757
+ );
625
758
  }
626
759
 
627
760
  function isNativeTurnActivity(event: OmpRpcEvent): boolean {
@@ -668,7 +801,7 @@ function createActiveTurn(
668
801
  clientMessageId: string,
669
802
  text: string,
670
803
  generation: number,
671
- terminalOwnershipRequired: boolean,
804
+ terminalPolicy: TerminalCorrelation["policy"],
672
805
  manualCompaction = false,
673
806
  ): ActiveTurn {
674
807
  return {
@@ -693,17 +826,18 @@ function createActiveTurn(
693
826
  manualCompaction,
694
827
  activitySequence: 0,
695
828
  acknowledged: false,
696
- terminalOwnershipEvidence: false,
829
+ terminalCorrelation: { policy: terminalPolicy, evidence: new Map() },
697
830
  replayingBufferedEvents: false,
698
- bufferedTerminalOwnershipEvidence: false,
699
- terminalOwnershipRequired,
700
831
  steersInFlight: 0,
832
+ activeToolCallIds: new Set(),
701
833
  steerReady: Promise.withResolvers<void>(),
702
834
  userCorrelationActive: false,
703
835
  userLookups: new Set(),
704
836
  userEchoes: [],
705
837
  completedMessageCount: 0,
706
838
  bufferedEvents: [],
839
+ streamedMessageEntryIds: [],
840
+ streamedMessageIdentityComplete: true,
707
841
  pendingUsers: [
708
842
  {
709
843
  clientMessageId,
@@ -715,6 +849,35 @@ function createActiveTurn(
715
849
  ],
716
850
  };
717
851
  }
852
+ function classifyTerminalCandidate(
853
+ turn: ActiveTurn,
854
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
855
+ arrivalSequence = turn.activitySequence,
856
+ ): TerminalCandidate {
857
+ if (event.requestId !== undefined) return { event, confidence: "keyed", arrivalSequence };
858
+ if (turn.agentInvoked === false) return { event, confidence: "ambiguous", arrivalSequence };
859
+ if (turn.terminalCorrelation.policy === "initial-turn") {
860
+ return { event, confidence: "initial-turn", arrivalSequence };
861
+ }
862
+ const assistantSequence = turn.terminalCorrelation.evidence.get("current-assistant");
863
+ if (
864
+ turn.terminalCorrelation.policy === "native-command" &&
865
+ assistantSequence !== undefined &&
866
+ assistantSequence <= arrivalSequence
867
+ ) {
868
+ return { event, confidence: "native-command", arrivalSequence };
869
+ }
870
+ const userSequence = turn.terminalCorrelation.evidence.get("fresh-native-user");
871
+ if (
872
+ userSequence !== undefined &&
873
+ assistantSequence !== undefined &&
874
+ userSequence < assistantSequence &&
875
+ assistantSequence <= arrivalSequence
876
+ ) {
877
+ return { event, confidence: "ordered-legacy", arrivalSequence };
878
+ }
879
+ return { event, confidence: "ambiguous", arrivalSequence };
880
+ }
718
881
 
719
882
  export class OmpProviderSession {
720
883
  readonly id: string;
@@ -731,6 +894,7 @@ export class OmpProviderSession {
731
894
  private readonly emittedEntryIds = new BoundedStringSet(MAX_TRACKED_ENTRY_IDS);
732
895
  private readonly seenEntryIds = new BoundedStringSet(MAX_TRACKED_ENTRY_IDS);
733
896
  private branchWatermarkValid = true;
897
+ private readonly branchEntryIds = new Set<string>();
734
898
  private readonly unclaimedBranchEntries: Array<{ entryId: string; text: string }> = [];
735
899
  private readonly scheduler: OmpTimelineScheduler;
736
900
  private readonly dataFilter: OmpPublicDataSerializer;
@@ -782,7 +946,7 @@ export class OmpProviderSession {
782
946
  private recoveryOptions: OmpRecoveryOptions,
783
947
  private readonly hostTools: OmpHostToolsBridge,
784
948
  private nativeSessionId: string,
785
- nativeSessionFile: string | undefined,
949
+ private readonly nativeSessionFile: string | undefined,
786
950
  private readonly config: ProviderSessionConfig,
787
951
  private configState: ProviderConfigState,
788
952
  outputRedactionValues: readonly string[],
@@ -813,6 +977,7 @@ export class OmpProviderSession {
813
977
  scheduler,
814
978
  outputRedactionValues,
815
979
  capabilities.includes("session.revert.conversation"),
980
+ capabilities.includes("timeline.plugin"),
816
981
  hostTools.labels,
817
982
  );
818
983
  this.subsessions = capabilities.includes("session.subsession")
@@ -825,6 +990,7 @@ export class OmpProviderSession {
825
990
  scheduler,
826
991
  () => this.resumeDeferredAgentEnd(),
827
992
  outputRedactionValues,
993
+ capabilities.includes("timeline.plugin"),
828
994
  )
829
995
  : null;
830
996
  this.bindRuntime(runtime);
@@ -832,6 +998,13 @@ export class OmpProviderSession {
832
998
  get persistenceSessionId(): string | undefined {
833
999
  return this.persistSession ? this.nativeSessionId : undefined;
834
1000
  }
1001
+ async openPaseoBrowser(url: string): Promise<void> {
1002
+ if (this.closed) throw new OmpPublicError("The OMP session is closed");
1003
+ await this.hostTools.openPaseoBrowser(url);
1004
+ }
1005
+ setBrowserAuthorizationIssuer(issue: ((url: string) => string | undefined) | null): void {
1006
+ this.projector.setBrowserAuthorizationIssuer(issue);
1007
+ }
835
1008
 
836
1009
  private readonly imageMaterializer = new OmpImageMaterializer();
837
1010
  static async open(
@@ -976,10 +1149,12 @@ export class OmpProviderSession {
976
1149
  }
977
1150
  const configState: ProviderConfigState = {
978
1151
  ...(state.model ? { model: ompModelId(state.model) } : {}),
979
- mode: normalizedConfig.mode,
1152
+ mode: normalizedConfig.mode ?? "full",
980
1153
  ...(committedThinkingLevel ? { thinkingOption: committedThinkingLevel } : {}),
981
1154
  models,
982
- modes: OMP_MODES,
1155
+ // OMP fixes approval mode at process launch. Publish only the selected mode so
1156
+ // Paseo shows the security state without offering unsupported transitions.
1157
+ modes: [fixedSessionMode(normalizedConfig.mode)],
983
1158
  thinkingOptions,
984
1159
  settings: [],
985
1160
  };
@@ -1093,7 +1268,7 @@ export class OmpProviderSession {
1093
1268
  ...(this.config.title ? { title: this.dataFilter.text(this.config.title, 256) } : {}),
1094
1269
  });
1095
1270
  this.emit({ type: "session.config", sessionId: this.id, config: this.configState });
1096
- if (this.replayHistoryOnOpen) await this.replayHistory();
1271
+ if (this.replayHistoryOnOpen) await this.replayHistory(true);
1097
1272
  this.publishCommands(this.commandCatalog);
1098
1273
  this.emit({ type: "session.ready", requestId, sessionId: this.id });
1099
1274
  this.readyPublished = true;
@@ -1383,7 +1558,7 @@ export class OmpProviderSession {
1383
1558
  });
1384
1559
  }
1385
1560
 
1386
- private async replayHistory(): Promise<void> {
1561
+ private async replayHistory(preferPersistedTranscript = false): Promise<void> {
1387
1562
  if (!this.runtime.canReplayHistory) {
1388
1563
  throw new OmpPublicError("OMP session history cannot be replayed safely");
1389
1564
  }
@@ -1397,19 +1572,48 @@ export class OmpProviderSession {
1397
1572
  this.replayTimeoutMs,
1398
1573
  );
1399
1574
  try {
1400
- const messages = await waitForReplay(this.runtime.getMessages(), replay.signal);
1401
- replay.signal.throwIfAborted();
1402
- if (messages.length > MAX_REPLAY_MESSAGES) {
1403
- throw new OmpPublicError("OMP session history exceeds replay limits");
1575
+ let messages: OmpMessage[] | undefined;
1576
+ // OMP's RPC history is model context, which excludes failed/aborted turns. On initial
1577
+ // resume/import prefer the already authorized journal; rewinds still use the runtime's
1578
+ // in-memory branch because an uncommitted leaf move is not represented by file order.
1579
+ if (
1580
+ preferPersistedTranscript &&
1581
+ this.nativeSessionFile &&
1582
+ this.runtimeFactory.readPersistedSessionTranscript
1583
+ ) {
1584
+ try {
1585
+ const transcript = await waitForReplay(
1586
+ this.runtimeFactory.readPersistedSessionTranscript({
1587
+ sessionFile: this.nativeSessionFile,
1588
+ sessionId: this.nativeSessionId,
1589
+ cwd: this.cwd,
1590
+ signal: replay.signal,
1591
+ }),
1592
+ replay.signal,
1593
+ );
1594
+ messages = transcript.messages;
1595
+ } catch (error) {
1596
+ if (replay.signal.aborted) throw error;
1597
+ this.emit({
1598
+ type: "timeline.item",
1599
+ sessionId: this.id,
1600
+ item: {
1601
+ id: "omp:replay-incomplete",
1602
+ type: "error",
1603
+ message:
1604
+ "OMP could not read its complete persisted transcript; displayed history may be incomplete.",
1605
+ },
1606
+ });
1607
+ }
1404
1608
  }
1405
- this.unclaimedBranchEntries.length = 0;
1609
+ messages ??= await waitForReplay(this.runtime.getMessages(), replay.signal);
1610
+ this.quarantineBranchEntries();
1406
1611
  for (const message of messages) {
1407
1612
  replay.signal.throwIfAborted();
1408
1613
  const entryId = nativeEntryId(message);
1409
1614
  if (entryId) this.seenEntryIds.add(entryId);
1410
1615
  this.projector.projectReplayMessage(message);
1411
1616
  }
1412
- this.branchWatermarkValid = true;
1413
1617
  this.projector.finishReplay();
1414
1618
  await this.subsessions?.replay(messages, this.runtime, this.runtimeFactory, replay.signal);
1415
1619
  replay.signal.throwIfAborted();
@@ -1705,7 +1909,7 @@ export class OmpProviderSession {
1705
1909
  input.prompt.clientMessageId,
1706
1910
  payload.text,
1707
1911
  this.generation,
1708
- this.runtimeTurnCompleted,
1912
+ this.runtimeTurnCompleted ? "ordered-legacy" : "initial-turn",
1709
1913
  slashCommandName(payload.text) === "compact",
1710
1914
  );
1711
1915
  this.activeTurn = turn;
@@ -1741,9 +1945,32 @@ export class OmpProviderSession {
1741
1945
  }, COMPACTION_MAX_WAIT_MS);
1742
1946
  return;
1743
1947
  }
1744
- const acknowledgement = await runtime.prompt(payload.text, payload.images, () => {
1745
- turn.promptAcceptedEventIndex ??= turn.bufferedEvents.length;
1746
- });
1948
+ const ownershipPending = turn.pendingUsers[0];
1949
+ if (
1950
+ turn.terminalCorrelation.policy === "ordered-legacy" &&
1951
+ !this.branchWatermarkValid &&
1952
+ ownershipPending
1953
+ ) {
1954
+ await this.refreshBranchEntries(turn, ownershipPending);
1955
+ if (
1956
+ this.closed ||
1957
+ turn.terminal ||
1958
+ this.activeTurn !== turn ||
1959
+ turn.generation !== this.generation
1960
+ ) {
1961
+ return;
1962
+ }
1963
+ }
1964
+ const acknowledgement = await runtime.prompt(
1965
+ payload.text,
1966
+ payload.images,
1967
+ () => {
1968
+ turn.promptAcceptedEventIndex ??= turn.bufferedEvents.length;
1969
+ },
1970
+ (requestId) => {
1971
+ turn.nativeRequestId = requestId;
1972
+ },
1973
+ );
1747
1974
  if (this.closed || turn.terminal) return;
1748
1975
  turn.nativeRequestId = acknowledgement.requestId;
1749
1976
  this.publishPromptResult(turn, { type: "turn", turnId: turn.turnId });
@@ -1761,13 +1988,10 @@ export class OmpProviderSession {
1761
1988
  0,
1762
1989
  turn.promptAcceptedEventIndex ?? bufferedEvents.length,
1763
1990
  );
1764
- for (const event of preAcceptanceEvents) this.handleTurnEvent(turn, event);
1991
+ for (const event of preAcceptanceEvents) this.handleTurnEvent(turn, event, false);
1765
1992
  this.projector.acceptLiveTurn(turn.turnId);
1766
1993
  for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
1767
1994
  turn.replayingBufferedEvents = false;
1768
- if (acknowledgement.agentInvoked === true && turn.bufferedTerminalOwnershipEvidence) {
1769
- this.markTerminalOwnershipEvidence(turn);
1770
- }
1771
1995
  if (
1772
1996
  acknowledgement.agentInvoked === false &&
1773
1997
  turn.agentInvoked !== true &&
@@ -1915,11 +2139,26 @@ export class OmpProviderSession {
1915
2139
  text: string,
1916
2140
  invoke: () => Promise<void>,
1917
2141
  ): Promise<void> {
1918
- const turn = createActiveTurn(clientMessageId, text, this.generation, false);
2142
+ if (this.activeTurn) {
2143
+ throw new OmpPublicError("OMP already has an active turn; send this message as a steer");
2144
+ }
2145
+ const turn = createActiveTurn(clientMessageId, text, this.generation, "native-command");
1919
2146
  this.activeTurn = turn;
1920
2147
  try {
1921
2148
  await invoke();
1922
- if (this.closed || turn.terminal || this.activeTurn !== turn) return;
2149
+ if (this.closed || turn.terminal || this.activeTurn !== turn) {
2150
+ if (!turn.terminal) {
2151
+ this.publishPendingUsers(turn);
2152
+ this.publishPromptResult(turn, {
2153
+ type: "failed",
2154
+ error: {
2155
+ message: this.closed ? "OMP session is closed" : "OMP command lost turn ownership",
2156
+ },
2157
+ });
2158
+ this.settleUnstartedTurn(turn);
2159
+ }
2160
+ return;
2161
+ }
1923
2162
  this.publishPromptResult(turn, { type: "turn", turnId: turn.turnId });
1924
2163
  turn.starting = false;
1925
2164
  turn.acknowledged = true;
@@ -1930,16 +2169,13 @@ export class OmpProviderSession {
1930
2169
  for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
1931
2170
  turn.replayingBufferedEvents = false;
1932
2171
  } catch (error) {
1933
- if (turn.terminal || this.activeTurn !== turn) return;
2172
+ if (turn.terminal) return;
2173
+ const ownsTurn = this.activeTurn === turn;
1934
2174
  const failure = providerError(error, "OMP command failed");
1935
2175
  this.publishPendingUsers(turn);
1936
2176
  this.publishPromptResult(turn, { type: "failed", error: failure });
1937
- turn.steerReady.resolve();
1938
- turn.terminal = true;
1939
- this.imageMaterializer.clear();
1940
- this.resolveTurnPermissions(turn.turnId);
1941
- this.projector.finishTurn(turn.turnId);
1942
- this.activeTurn = null;
2177
+ this.settleUnstartedTurn(turn);
2178
+ if (ownsTurn) this.imageMaterializer.clear();
1943
2179
  }
1944
2180
  }
1945
2181
 
@@ -2040,6 +2276,14 @@ export class OmpProviderSession {
2040
2276
  this.lifetime.abort(new Error("OMP provider connection closed"));
2041
2277
  this.unsubscribe();
2042
2278
  this.unsubscribe = () => {};
2279
+ const turn = this.activeTurn;
2280
+ if (turn && !turn.started) {
2281
+ this.publishPromptResult(turn, {
2282
+ type: "failed",
2283
+ error: { message: "OMP session closed before the prompt was accepted" },
2284
+ });
2285
+ this.settleUnstartedTurn(turn);
2286
+ }
2043
2287
  }
2044
2288
  async configure(input: SessionConfigureInput): Promise<void> {
2045
2289
  if (this.revertInFlight) {
@@ -2296,6 +2540,23 @@ export class OmpProviderSession {
2296
2540
  }
2297
2541
  }
2298
2542
 
2543
+ private async readRuntimeHistoryWithTimeout(
2544
+ runtime: OmpRuntimeSession,
2545
+ ): Promise<OmpMessage[] | undefined> {
2546
+ if (!runtime.canReplayHistory) return undefined;
2547
+ const historyRequest = runtime.getMessages();
2548
+ void historyRequest.catch(() => undefined);
2549
+ const timeout = Promise.withResolvers<null>();
2550
+ const timer = this.scheduler.set(() => timeout.resolve(null), AGENT_END_HISTORY_TIMEOUT_MS);
2551
+ try {
2552
+ return (await Promise.race([historyRequest, timeout.promise])) ?? undefined;
2553
+ } catch {
2554
+ return undefined;
2555
+ } finally {
2556
+ this.scheduler.clear(timer);
2557
+ }
2558
+ }
2559
+
2299
2560
  private publishCommittedConfig(
2300
2561
  state: OmpSessionState,
2301
2562
  runtime: OmpRuntimeSession,
@@ -2499,12 +2760,8 @@ export class OmpProviderSession {
2499
2760
  });
2500
2761
  if (turn.started) await this.finishTurn(turn, "canceled", undefined, true, true);
2501
2762
  else {
2502
- turn.steerReady.resolve();
2503
- turn.terminal = true;
2504
- this.stopUsagePoll(turn);
2763
+ this.settleUnstartedTurn(turn);
2505
2764
  this.finishCompaction("canceled");
2506
- this.projector.finishTurn(turn.turnId);
2507
- if (this.activeTurn === turn) this.activeTurn = null;
2508
2765
  }
2509
2766
  }
2510
2767
  this.imageMaterializer.clear();
@@ -2783,6 +3040,7 @@ export class OmpProviderSession {
2783
3040
  return;
2784
3041
  }
2785
3042
  turn.localOnlyDisabled = true;
3043
+ this.cancelAmbiguousTerminal(turn);
2786
3044
  turn.deferredAgentEnd = undefined;
2787
3045
  this.acceptPendingUser(turn, pending);
2788
3046
  this.emit({
@@ -2905,6 +3163,13 @@ export class OmpProviderSession {
2905
3163
  }
2906
3164
  return;
2907
3165
  }
3166
+ if (
3167
+ event.type === "agent_end" &&
3168
+ event.requestId !== undefined &&
3169
+ event.requestId !== turn.nativeRequestId
3170
+ ) {
3171
+ return;
3172
+ }
2908
3173
  if (turn.starting) {
2909
3174
  if (
2910
3175
  turn.bufferedEvents.length >= MAX_BUFFERED_TURN_EVENTS ||
@@ -2927,7 +3192,11 @@ export class OmpProviderSession {
2927
3192
  this.handleTurnEvent(turn, event);
2928
3193
  }
2929
3194
 
2930
- private handleTurnEvent(turn: ActiveTurn, event: OmpRpcEvent): void {
3195
+ private handleTurnEvent(
3196
+ turn: ActiveTurn,
3197
+ event: OmpRpcEvent,
3198
+ correlatesToAcceptedPrompt = true,
3199
+ ): void {
2931
3200
  if (
2932
3201
  turn.generation !== this.generation ||
2933
3202
  turn.terminal ||
@@ -2936,16 +3205,32 @@ export class OmpProviderSession {
2936
3205
  ) {
2937
3206
  return;
2938
3207
  }
2939
- if (event.type === "message_end") {
3208
+ if (
3209
+ event.type === "agent_end" &&
3210
+ event.requestId !== undefined &&
3211
+ event.requestId !== turn.nativeRequestId
3212
+ ) {
3213
+ return;
3214
+ }
3215
+ if (event.type === "message_end" && correlatesToAcceptedPrompt) {
3216
+ const entryId = nativeEntryId(event.message);
3217
+ if (!entryId || turn.streamedMessageEntryIds.length >= MAX_AGENT_END_CORRELATION_MESSAGES) {
3218
+ turn.streamedMessageIdentityComplete = false;
3219
+ } else {
3220
+ turn.streamedMessageEntryIds.push(entryId);
3221
+ }
2940
3222
  turn.completedMessageCount += 1;
2941
3223
  if (event.message.role === "assistant") {
2942
- turn.lastCompletedAssistantFailed =
2943
- event.message.stopReason === "error" || !!event.message.errorMessage;
3224
+ turn.lastCompletedAssistantOutcome = assistantTerminalOutcome(event.message);
3225
+ turn.lastCompletedAssistantEntryId = entryId;
2944
3226
  }
2945
3227
  }
2946
3228
  if (event.type === "prompt_error") {
2947
3229
  if (event.id !== turn.nativeRequestId) return;
2948
- const error = { message: event.error };
3230
+ const error: ProviderError = {
3231
+ message: this.dataFilter.text(event.error, 4_096),
3232
+ ...(event.code ? { code: this.dataFilter.text(event.code, 256) } : {}),
3233
+ };
2949
3234
  this.publishPendingUsers(turn);
2950
3235
  void this.finishTurn(turn, "failed", error);
2951
3236
  return;
@@ -2953,15 +3238,19 @@ export class OmpProviderSession {
2953
3238
  if (event.type === "prompt_result") {
2954
3239
  if (!event.id || event.id !== turn.nativeRequestId) return;
2955
3240
  if (event.agentInvoked) {
3241
+ // A request-keyed prompt result proves dispatch, but an unkeyed terminal still needs
3242
+ // ordered native user and assistant evidence from this accepted prompt.
2956
3243
  this.markAgentEvidence(turn);
2957
- if (turn.replayingBufferedEvents) turn.bufferedTerminalOwnershipEvidence = true;
2958
- else this.markTerminalOwnershipEvidence(turn);
2959
3244
  return;
2960
3245
  }
2961
3246
  if (turn.localOnlyDisabled || turn.steersInFlight > 0) return;
2962
3247
  if (!turn.nativeActivity && !turn.awaitingPermissionEvidence) {
2963
3248
  turn.agentInvoked = false;
2964
3249
  turn.localOnlyEligible = true;
3250
+ if (turn.deferredAgentEnd?.confidence === "ambiguous") {
3251
+ turn.deferredAgentEnd = undefined;
3252
+ this.cancelAmbiguousTerminal(turn);
3253
+ }
2965
3254
  this.scheduleLocalOnlyCompletion(turn);
2966
3255
  }
2967
3256
  return;
@@ -3009,8 +3298,10 @@ export class OmpProviderSession {
3009
3298
  return;
3010
3299
  }
3011
3300
  if (event.type === "agent_end" && turn.manualCompactionPending) return;
3012
- if (event.type === "tool_execution_end" && event.toolName === "ask_user") {
3013
- this.pendingFreeformSelection = null;
3301
+ if (event.type === "tool_execution_start") turn.activeToolCallIds.add(event.toolCallId);
3302
+ if (event.type === "tool_execution_end") {
3303
+ turn.activeToolCallIds.delete(event.toolCallId);
3304
+ if (event.toolName === "ask_user") this.pendingFreeformSelection = null;
3014
3305
  }
3015
3306
  if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
3016
3307
  try {
@@ -3020,6 +3311,37 @@ export class OmpProviderSession {
3020
3311
  return;
3021
3312
  }
3022
3313
  }
3314
+ if (event.type === "agent_end") {
3315
+ if (event.isTerminal === false) {
3316
+ turn.completedMessageCount = 0;
3317
+ turn.streamedMessageEntryIds.length = 0;
3318
+ turn.streamedMessageIdentityComplete = true;
3319
+ turn.lastCompletedAssistantOutcome = undefined;
3320
+ turn.lastCompletedAssistantEntryId = undefined;
3321
+ return;
3322
+ }
3323
+ const candidate: TerminalCandidate = turn.interrupted
3324
+ ? { event, confidence: "interrupted", arrivalSequence: turn.activitySequence }
3325
+ : classifyTerminalCandidate(turn, event);
3326
+ if (candidate.confidence === "ambiguous") {
3327
+ this.deferAmbiguousTerminal(turn, candidate);
3328
+ return;
3329
+ }
3330
+ turn.nativeActivity = true;
3331
+ this.cancelLocalOnlyCompletion(turn);
3332
+ if (
3333
+ (!turn.interrupted &&
3334
+ candidate.confidence !== "keyed" &&
3335
+ this.hasTerminalConflict(turn, false)) ||
3336
+ turn.steersInFlight > 0 ||
3337
+ turn.terminalizing
3338
+ ) {
3339
+ turn.deferredAgentEnd = candidate;
3340
+ return;
3341
+ }
3342
+ this.beginTerminalization(turn, candidate);
3343
+ return;
3344
+ }
3023
3345
  if (isNativeTurnActivity(event)) {
3024
3346
  turn.nativeActivity = true;
3025
3347
  this.cancelLocalOnlyCompletion(turn);
@@ -3036,34 +3358,13 @@ export class OmpProviderSession {
3036
3358
  event.message.role === "assistant"
3037
3359
  ) {
3038
3360
  turn.awaitingPermissionEvidence = false;
3039
- }
3040
- if (event.type === "agent_end") {
3041
- const hasAssistantEvidence =
3042
- event.messages?.some((message) => message.role === "assistant") ?? false;
3043
- if (turn.awaitingPermissionEvidence && !hasAssistantEvidence) {
3044
- turn.deferredAgentEnd = event;
3045
- return;
3361
+ if (correlatesToAcceptedPrompt) {
3362
+ turn.terminalCorrelation.evidence.set("current-assistant", turn.activitySequence + 1);
3046
3363
  }
3047
- if (hasAssistantEvidence) turn.awaitingPermissionEvidence = false;
3048
- if (event.isTerminal === false) return;
3049
- if (turn.terminalizing) {
3050
- turn.deferredAgentEnd = event;
3051
- return;
3052
- }
3053
- if (turn.steersInFlight > 0) {
3054
- turn.deferredAgentEnd = event;
3055
- return;
3056
- }
3057
- this.beginTerminalization(turn, event);
3058
- return;
3059
3364
  }
3060
3365
  if (isNativeTurnActivity(event)) this.markAgentEvidence(turn);
3061
- if (event.type === "message_end" && event.message.role === "user") {
3062
- this.markAgentEvidence(turn);
3063
- this.projectUserEcho(turn, event.message);
3064
- return;
3065
- }
3066
3366
  this.projector.project(event, turn.turnId);
3367
+ if (event.type === "tool_execution_end") this.resumeDeferredAgentEnd();
3067
3368
  }
3068
3369
 
3069
3370
  private projectUserEcho(turn: ActiveTurn, message: OmpMessage): void {
@@ -3134,52 +3435,10 @@ export class OmpProviderSession {
3134
3435
  pending.bufferedEchoes.push(...turn.userEchoes.splice(0));
3135
3436
  return;
3136
3437
  }
3137
- let resolvedId = entryId ?? this.claimUnclaimedBranchEntry(pending.text);
3138
- if (!resolvedId) {
3139
- try {
3140
- const messages = await this.runtime.getBranchMessages();
3141
- if (
3142
- this.closed ||
3143
- turn.terminal ||
3144
- this.activeTurn !== turn ||
3145
- turn.pendingUsers[0] !== pending
3146
- ) {
3147
- return;
3148
- }
3149
- if (retainedBytes(messages, MAX_UNCLAIMED_BRANCH_BYTES) === Number.POSITIVE_INFINITY) {
3150
- this.quarantineBranchEntries();
3151
- return;
3152
- }
3153
- const unseen: Array<{ entryId: string; text: string }> = [];
3154
- for (const branchMessage of messages) {
3155
- if (!this.seenEntryIds.has(branchMessage.entryId)) unseen.push(branchMessage);
3156
- }
3157
- if (!this.branchWatermarkValid) {
3158
- this.unclaimedBranchEntries.length = 0;
3159
- this.branchWatermarkValid = true;
3160
- } else if (
3161
- unseen.length <= MAX_UNCLAIMED_BRANCH_ENTRIES - this.unclaimedBranchEntries.length &&
3162
- retainedBytes(this.unclaimedBranchEntries, MAX_UNCLAIMED_BRANCH_BYTES) +
3163
- retainedBytes(unseen, MAX_UNCLAIMED_BRANCH_BYTES) <=
3164
- MAX_UNCLAIMED_BRANCH_BYTES
3165
- ) {
3166
- this.unclaimedBranchEntries.push(...unseen);
3167
- } else {
3168
- this.quarantineBranchEntries();
3169
- }
3170
- for (const branchMessage of messages) this.seenEntryIds.add(branchMessage.entryId);
3171
- resolvedId = this.claimUnclaimedBranchEntry(pending.text);
3172
- } catch {
3173
- if (
3174
- this.closed ||
3175
- turn.terminal ||
3176
- this.activeTurn !== turn ||
3177
- turn.pendingUsers[0] !== pending
3178
- ) {
3179
- return;
3180
- }
3181
- this.quarantineBranchEntries();
3182
- }
3438
+ const observedSequence = turn.activitySequence;
3439
+ let resolvedId = entryId ?? this.claimUnclaimedBranchEntry(turn, pending.text);
3440
+ if (!resolvedId && (await this.refreshBranchEntries(turn, pending))) {
3441
+ resolvedId = this.claimUnclaimedBranchEntry(turn, pending.text);
3183
3442
  }
3184
3443
  if (
3185
3444
  this.closed ||
@@ -3192,18 +3451,94 @@ export class OmpProviderSession {
3192
3451
  turn.userEchoes.shift();
3193
3452
  if (!resolvedId) return;
3194
3453
  turn.pendingUsers.shift();
3195
- this.publishCorrelatedUser(turn, pending, resolvedId);
3454
+ this.publishCorrelatedUser(turn, pending, resolvedId, observedSequence);
3455
+ }
3456
+ }
3457
+ private async refreshBranchEntries(turn: ActiveTurn, pending: PendingUser): Promise<boolean> {
3458
+ const runtime = this.runtime;
3459
+ try {
3460
+ const messages = await runtime.getBranchMessages();
3461
+ if (
3462
+ this.closed ||
3463
+ turn.terminal ||
3464
+ this.activeTurn !== turn ||
3465
+ turn.pendingUsers[0] !== pending ||
3466
+ turn.generation !== this.generation ||
3467
+ runtime !== this.runtime
3468
+ ) {
3469
+ return false;
3470
+ }
3471
+ if (
3472
+ messages.length > MAX_UNCLAIMED_BRANCH_ENTRIES ||
3473
+ retainedBytes(messages, MAX_UNCLAIMED_BRANCH_BYTES) === Number.POSITIVE_INFINITY
3474
+ ) {
3475
+ this.quarantineBranchEntries();
3476
+ return false;
3477
+ }
3478
+ const unseen: Array<{ entryId: string; text: string }> = [];
3479
+ const snapshotIds = new Set<string>();
3480
+ for (const message of messages) {
3481
+ if (snapshotIds.has(message.entryId)) {
3482
+ this.quarantineBranchEntries();
3483
+ return false;
3484
+ }
3485
+ snapshotIds.add(message.entryId);
3486
+ if (!this.branchEntryIds.has(message.entryId)) unseen.push(message);
3487
+ }
3488
+ if (!this.branchWatermarkValid) {
3489
+ this.unclaimedBranchEntries.length = 0;
3490
+ this.branchWatermarkValid = true;
3491
+ } else if (
3492
+ unseen.length <= MAX_UNCLAIMED_BRANCH_ENTRIES - this.unclaimedBranchEntries.length &&
3493
+ this.branchEntryIds.size + unseen.length <= MAX_UNCLAIMED_BRANCH_ENTRIES &&
3494
+ retainedBytes(this.unclaimedBranchEntries, MAX_UNCLAIMED_BRANCH_BYTES) +
3495
+ retainedBytes(unseen, MAX_UNCLAIMED_BRANCH_BYTES) <=
3496
+ MAX_UNCLAIMED_BRANCH_BYTES
3497
+ ) {
3498
+ this.unclaimedBranchEntries.push(...unseen);
3499
+ } else {
3500
+ this.quarantineBranchEntries();
3501
+ return false;
3502
+ }
3503
+ for (const message of messages) {
3504
+ this.branchEntryIds.add(message.entryId);
3505
+ this.seenEntryIds.add(message.entryId);
3506
+ }
3507
+ return true;
3508
+ } catch {
3509
+ if (
3510
+ !this.closed &&
3511
+ !turn.terminal &&
3512
+ this.activeTurn === turn &&
3513
+ turn.pendingUsers[0] === pending &&
3514
+ turn.generation === this.generation &&
3515
+ runtime === this.runtime
3516
+ ) {
3517
+ this.quarantineBranchEntries();
3518
+ }
3519
+ return false;
3196
3520
  }
3197
3521
  }
3198
3522
 
3199
- private claimUnclaimedBranchEntry(text: string): string | undefined {
3523
+ private claimUnclaimedBranchEntry(turn: ActiveTurn, text: string): string | undefined {
3200
3524
  const index = this.unclaimedBranchEntries.findIndex((entry) => entry.text === text);
3201
3525
  if (index < 0) return undefined;
3526
+ let matches = 0;
3527
+ let expected = 0;
3528
+ for (const entry of this.unclaimedBranchEntries) if (entry.text === text) matches += 1;
3529
+ for (const pending of turn.pendingUsers) {
3530
+ if (pending.accepted && pending.text === text) expected += 1;
3531
+ }
3532
+ if (matches > expected) {
3533
+ this.quarantineBranchEntries();
3534
+ return undefined;
3535
+ }
3202
3536
  return this.unclaimedBranchEntries.splice(index, 1)[0]?.entryId;
3203
3537
  }
3204
3538
 
3205
3539
  private quarantineBranchEntries(): void {
3206
3540
  this.unclaimedBranchEntries.length = 0;
3541
+ this.branchEntryIds.clear();
3207
3542
  this.branchWatermarkValid = false;
3208
3543
  }
3209
3544
 
@@ -3950,23 +4285,27 @@ export class OmpProviderSession {
3950
4285
  return this.activeTurn?.turnId === pending.turnId && !this.activeTurn.terminal;
3951
4286
  }
3952
4287
 
3953
- private reevaluateDeferredPermissionTerminal(): void {
3954
- const turn = this.activeTurn;
3955
- if (!turn?.deferredAgentEnd || turn.terminal || turn.terminalizing) return;
4288
+ private hasTerminalConflict(turn: ActiveTurn, includeChildren = true): boolean {
3956
4289
  const ownsTurn = (pending: PendingPermission | PendingToolPermission) =>
3957
4290
  pending.turnId === turn.turnId;
3958
- if (
4291
+ return (
4292
+ turn.awaitingPermissionEvidence ||
3959
4293
  [...this.pendingPermissions.values()].some(ownsTurn) ||
3960
4294
  [...this.inFlightPermissions.values()].some(ownsTurn) ||
3961
4295
  [...this.pendingToolPermissions.values()].some(ownsTurn) ||
3962
- [...this.inFlightToolPermissions.values()].some(ownsTurn)
3963
- ) {
3964
- return;
3965
- }
3966
- const hasAssistantEvidence =
3967
- turn.deferredAgentEnd.messages?.some((message) => message.role === "assistant") ?? false;
3968
- if (!hasAssistantEvidence || turn.awaitingPermissionEvidence) return;
4296
+ [...this.inFlightToolPermissions.values()].some(ownsTurn) ||
4297
+ turn.activeToolCallIds.size > 0 ||
4298
+ turn.steersInFlight > 0 ||
4299
+ (includeChildren && Boolean(this.subsessions?.hasActiveChildren()))
4300
+ );
4301
+ }
4302
+
4303
+ private reevaluateDeferredPermissionTerminal(): void {
4304
+ const turn = this.activeTurn;
4305
+ if (!turn?.deferredAgentEnd || turn.terminal || turn.terminalizing) return;
4306
+ if (this.hasTerminalConflict(turn)) return;
3969
4307
  const deferred = turn.deferredAgentEnd;
4308
+ if (deferred.confidence === "ambiguous") return;
3970
4309
  turn.deferredAgentEnd = undefined;
3971
4310
  this.beginTerminalization(turn, deferred);
3972
4311
  }
@@ -3982,12 +4321,23 @@ export class OmpProviderSession {
3982
4321
  return this.slashCommands.has(commandName);
3983
4322
  }
3984
4323
 
3985
- private publishCorrelatedUser(turn: ActiveTurn, pending: PendingUser, entryId?: string): void {
4324
+ private publishCorrelatedUser(
4325
+ turn: ActiveTurn,
4326
+ pending: PendingUser,
4327
+ entryId: string | undefined,
4328
+ observedSequence: number,
4329
+ ): void {
3986
4330
  if (entryId) {
3987
4331
  if (this.emittedEntryIds.has(entryId)) return;
3988
4332
  this.seenEntryIds.add(entryId);
3989
4333
  this.emittedEntryIds.add(entryId);
3990
- this.markTerminalOwnershipEvidence(turn);
4334
+ turn.terminalCorrelation.evidence.set("fresh-native-user", observedSequence);
4335
+ this.refreshAmbiguousTerminal(turn);
4336
+ if (this.branchWatermarkValid && !this.branchEntryIds.has(entryId)) {
4337
+ if (this.branchEntryIds.size >= MAX_UNCLAIMED_BRANCH_ENTRIES)
4338
+ this.quarantineBranchEntries();
4339
+ else this.branchEntryIds.add(entryId);
4340
+ }
3991
4341
  const unclaimedIndex = this.unclaimedBranchEntries.findIndex(
3992
4342
  (entry) => entry.entryId === entryId,
3993
4343
  );
@@ -4002,12 +4352,7 @@ export class OmpProviderSession {
4002
4352
  turn.activitySequence += 1;
4003
4353
  turn.localOnlyEligible = false;
4004
4354
  this.cancelLocalOnlyCompletion(turn);
4005
- if (!turn.awaitingPermissionEvidence) turn.deferredAgentEnd = undefined;
4006
- }
4007
-
4008
- private markTerminalOwnershipEvidence(turn: ActiveTurn): void {
4009
- turn.terminalOwnershipEvidence = true;
4010
- this.cancelTerminalOwnershipTimeout(turn);
4355
+ this.refreshAmbiguousTerminal(turn);
4011
4356
  }
4012
4357
 
4013
4358
  private scheduleLocalOnlyCompletion(turn: ActiveTurn): void {
@@ -4033,32 +4378,68 @@ export class OmpProviderSession {
4033
4378
  turn.localOnlyTimer = undefined;
4034
4379
  }
4035
4380
 
4036
- private scheduleTerminalOwnershipTimeout(turn: ActiveTurn): void {
4037
- if (
4038
- turn.terminalOwnershipTimer !== undefined ||
4039
- turn.terminalOwnershipEvidence ||
4040
- (turn.agentInvoked === false && turn.localOnlyEligible)
4041
- ) {
4381
+ private refreshAmbiguousTerminal(turn: ActiveTurn): void {
4382
+ const candidate = turn.deferredAgentEnd;
4383
+ if (candidate?.confidence !== "ambiguous") return;
4384
+ const resolved = classifyTerminalCandidate(turn, candidate.event, candidate.arrivalSequence);
4385
+ if (resolved.confidence !== "ambiguous") {
4386
+ this.cancelAmbiguousTerminal(turn);
4387
+ turn.deferredAgentEnd = undefined;
4388
+ if (turn.terminalizing || this.hasTerminalConflict(turn, false)) {
4389
+ turn.deferredAgentEnd = resolved;
4390
+ } else {
4391
+ this.beginTerminalization(turn, resolved);
4392
+ }
4042
4393
  return;
4043
4394
  }
4044
- turn.terminalOwnershipTimer = this.scheduler.set(() => {
4045
- turn.terminalOwnershipTimer = undefined;
4046
- if (
4047
- this.closed ||
4048
- turn.terminal ||
4049
- this.activeTurn !== turn ||
4050
- turn.terminalOwnershipEvidence
4051
- ) {
4052
- return;
4053
- }
4054
- this.handleRuntimeFailure("OMP terminal ownership could not be confirmed");
4395
+ if (turn.activitySequence > candidate.arrivalSequence) {
4396
+ turn.deferredAgentEnd = undefined;
4397
+ this.cancelAmbiguousTerminal(turn);
4398
+ }
4399
+ }
4400
+
4401
+ private deferAmbiguousTerminal(turn: ActiveTurn, candidate: TerminalCandidate): void {
4402
+ if (turn.agentInvoked === false && turn.localOnlyEligible) return;
4403
+ this.cancelAmbiguousTerminal(turn);
4404
+ turn.deferredAgentEnd = candidate;
4405
+ turn.ambiguousTerminalTimer = this.scheduler.set(() => {
4406
+ turn.ambiguousTerminalTimer = undefined;
4407
+ void this.settleAmbiguousTerminal(turn, candidate);
4055
4408
  }, AGENT_END_STATE_TIMEOUT_MS);
4056
4409
  }
4057
4410
 
4058
- private cancelTerminalOwnershipTimeout(turn: ActiveTurn): void {
4059
- if (turn.terminalOwnershipTimer === undefined) return;
4060
- this.scheduler.clear(turn.terminalOwnershipTimer);
4061
- turn.terminalOwnershipTimer = undefined;
4411
+ private cancelAmbiguousTerminal(turn: ActiveTurn): void {
4412
+ if (turn.ambiguousTerminalTimer === undefined) return;
4413
+ this.scheduler.clear(turn.ambiguousTerminalTimer);
4414
+ turn.ambiguousTerminalTimer = undefined;
4415
+ }
4416
+
4417
+ private async settleAmbiguousTerminal(
4418
+ turn: ActiveTurn,
4419
+ candidate: TerminalCandidate,
4420
+ ): Promise<void> {
4421
+ if (
4422
+ this.closed ||
4423
+ turn.terminal ||
4424
+ this.activeTurn !== turn ||
4425
+ turn.deferredAgentEnd !== candidate
4426
+ ) {
4427
+ return;
4428
+ }
4429
+ await Promise.allSettled(turn.userLookups);
4430
+ this.refreshAmbiguousTerminal(turn);
4431
+ if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
4432
+ const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4433
+ if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
4434
+ if (!state) {
4435
+ this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
4436
+ return;
4437
+ }
4438
+ turn.deferredAgentEnd = undefined;
4439
+ if (state.isStreaming || state.isCompacting) return;
4440
+ await this.finishTurn(turn, "failed", {
4441
+ message: "OMP unkeyed agent_end could not be correlated to the current prompt",
4442
+ });
4062
4443
  }
4063
4444
 
4064
4445
  private async completeLocalOnlyTurn(turn: ActiveTurn): Promise<void> {
@@ -4084,15 +4465,44 @@ export class OmpProviderSession {
4084
4465
  await this.finishTurn(turn, "completed", undefined, false, false, true);
4085
4466
  }
4086
4467
 
4087
- private beginTerminalization(
4088
- turn: ActiveTurn,
4089
- event: Extract<OmpRpcEvent, { type: "agent_end" }>,
4090
- ): void {
4468
+ private resetAgentEndProbe(turn: ActiveTurn): void {
4469
+ turn.agentEndPending = false;
4470
+ turn.terminalizing = false;
4471
+ if (turn.agentEndRetryTimer !== undefined) {
4472
+ this.scheduler.clear(turn.agentEndRetryTimer);
4473
+ turn.agentEndRetryTimer = undefined;
4474
+ }
4475
+ if (turn.agentEndDeadlineTimer !== undefined) {
4476
+ this.scheduler.clear(turn.agentEndDeadlineTimer);
4477
+ turn.agentEndDeadlineTimer = undefined;
4478
+ }
4479
+ }
4480
+ private ignoreActiveTerminalCandidate(turn: ActiveTurn, candidate: TerminalCandidate): void {
4481
+ const replacement =
4482
+ turn.deferredAgentEnd && turn.deferredAgentEnd !== candidate
4483
+ ? turn.deferredAgentEnd
4484
+ : undefined;
4485
+ this.resetAgentEndProbe(turn);
4486
+ turn.deferredAgentEnd = replacement;
4487
+ if (replacement && replacement.confidence !== "ambiguous") {
4488
+ turn.deferredAgentEnd = undefined;
4489
+ this.beginTerminalization(turn, replacement);
4490
+ return;
4491
+ }
4492
+ this.pollUsage(turn);
4493
+ }
4494
+
4495
+ private beginTerminalization(turn: ActiveTurn, candidate: TerminalCandidate): void {
4091
4496
  if (turn.terminal || turn.terminalizing || turn.agentEndPending || this.activeTurn !== turn) {
4092
4497
  return;
4093
4498
  }
4094
- if (!turn.interrupted && this.deferAgentEndForSubsessions(turn, event)) return;
4499
+ if (!turn.interrupted && this.deferAgentEndForSubsessions(turn, candidate)) return;
4095
4500
  turn.agentEndPending = true;
4501
+ if (turn.deferredAgentEnd?.confidence === "ambiguous") {
4502
+ turn.deferredAgentEnd = undefined;
4503
+ }
4504
+ turn.terminalizing = true;
4505
+ this.cancelAmbiguousTerminal(turn);
4096
4506
  turn.usageSampleFloor = this.usageSequence + 1;
4097
4507
  if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
4098
4508
  this.usageSample = null;
@@ -4101,19 +4511,22 @@ export class OmpProviderSession {
4101
4511
  turn.agentEndDeadlineTimer = this.scheduler.set(() => {
4102
4512
  turn.agentEndDeadlineTimer = undefined;
4103
4513
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
4104
- if (turn.userEchoObserved) {
4105
- this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
4514
+ if (
4515
+ candidate.confidence === "keyed" ||
4516
+ (candidate.confidence === "initial-turn" && !turn.userEchoObserved)
4517
+ ) {
4518
+ void this.completeAgentEnd(turn, candidate.event);
4106
4519
  } else {
4107
- void this.completeAgentEnd(turn, event);
4520
+ this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
4108
4521
  }
4109
4522
  }, AGENT_END_SETTLE_MS);
4110
- this.finishFromAgentEnd(turn, event);
4523
+ this.finishFromAgentEnd(turn, candidate);
4111
4524
  }
4112
4525
 
4113
4526
  private resumeAfterFailedSteer(turn: ActiveTurn): void {
4114
4527
  if (turn.terminal || this.activeTurn !== turn || turn.steersInFlight > 0) return;
4115
4528
  const deferred = turn.deferredAgentEnd;
4116
- if (deferred) {
4529
+ if (deferred && deferred.confidence !== "ambiguous" && !this.hasTerminalConflict(turn)) {
4117
4530
  turn.deferredAgentEnd = undefined;
4118
4531
  this.beginTerminalization(turn, deferred);
4119
4532
  return;
@@ -4122,13 +4535,11 @@ export class OmpProviderSession {
4122
4535
  this.scheduleLocalOnlyCompletion(turn);
4123
4536
  }
4124
4537
  }
4125
- private deferAgentEndForSubsessions(
4126
- turn: ActiveTurn,
4127
- event: Extract<OmpRpcEvent, { type: "agent_end" }>,
4128
- ): boolean {
4538
+
4539
+ private deferAgentEndForSubsessions(turn: ActiveTurn, candidate: TerminalCandidate): boolean {
4129
4540
  if (!this.subsessions?.hasActiveChildren()) return false;
4130
4541
  turn.terminalizing = false;
4131
- turn.deferredAgentEnd = event;
4542
+ turn.deferredAgentEnd = candidate;
4132
4543
  void this.subsessions.reconcile(this.runtime).catch(() => {
4133
4544
  if (!turn.terminal && this.activeTurn === turn) {
4134
4545
  this.handleRuntimeFailure("OMP subagent reconciliation failed");
@@ -4139,31 +4550,22 @@ export class OmpProviderSession {
4139
4550
 
4140
4551
  private resumeDeferredAgentEnd(): void {
4141
4552
  const turn = this.activeTurn;
4142
- if (
4143
- !turn ||
4144
- turn.terminal ||
4145
- turn.terminalizing ||
4146
- turn.steersInFlight > 0 ||
4147
- this.subsessions?.hasActiveChildren()
4148
- ) {
4553
+ if (!turn || turn.terminal || turn.terminalizing || this.hasTerminalConflict(turn)) {
4149
4554
  return;
4150
4555
  }
4151
- const event = turn.deferredAgentEnd;
4152
- if (!event) return;
4556
+ const candidate = turn.deferredAgentEnd;
4557
+ if (!candidate || candidate.confidence === "ambiguous") return;
4153
4558
  turn.deferredAgentEnd = undefined;
4154
- this.beginTerminalization(turn, event);
4559
+ this.beginTerminalization(turn, candidate);
4155
4560
  }
4156
4561
 
4157
- private finishFromAgentEnd(
4158
- turn: ActiveTurn,
4159
- event: Extract<OmpRpcEvent, { type: "agent_end" }>,
4160
- ): void {
4562
+ private finishFromAgentEnd(turn: ActiveTurn, candidate: TerminalCandidate): void {
4161
4563
  if (!turn.agentEndPending || turn.terminal) return;
4162
4564
  if (turn.agentEndCheck) {
4163
- turn.deferredAgentEnd = event;
4565
+ turn.deferredAgentEnd = candidate;
4164
4566
  return;
4165
4567
  }
4166
- const check = this.checkAgentEndState(turn, event);
4568
+ const check = this.checkAgentEndState(turn, candidate);
4167
4569
  turn.agentEndCheck = check;
4168
4570
  void check.finally(() => {
4169
4571
  if (turn.agentEndCheck !== check) return;
@@ -4175,10 +4577,7 @@ export class OmpProviderSession {
4175
4577
  });
4176
4578
  }
4177
4579
 
4178
- private async checkAgentEndState(
4179
- turn: ActiveTurn,
4180
- event: Extract<OmpRpcEvent, { type: "agent_end" }>,
4181
- ): Promise<void> {
4580
+ private async checkAgentEndState(turn: ActiveTurn, candidate: TerminalCandidate): Promise<void> {
4182
4581
  await Promise.allSettled(turn.userLookups);
4183
4582
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
4184
4583
  while (turn.userEchoes.length > 0) {
@@ -4187,37 +4586,42 @@ export class OmpProviderSession {
4187
4586
  await Promise.allSettled(turn.userLookups);
4188
4587
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
4189
4588
  }
4589
+ if (
4590
+ !turn.interrupted &&
4591
+ candidate.confidence !== "keyed" &&
4592
+ this.hasTerminalConflict(turn, false)
4593
+ ) {
4594
+ this.resetAgentEndProbe(turn);
4595
+ turn.deferredAgentEnd = candidate;
4596
+ this.pollUsage(turn);
4597
+ return;
4598
+ }
4190
4599
  if (turn.interrupted) {
4191
- await this.completeAgentEnd(turn, event);
4600
+ await this.completeAgentEnd(turn, candidate.event);
4192
4601
  return;
4193
4602
  }
4194
4603
  const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4195
4604
  if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
4196
4605
  if (!turn.interrupted && this.subsessions?.hasActiveChildren()) {
4197
- turn.agentEndPending = false;
4198
- if (this.deferAgentEndForSubsessions(turn, event)) return;
4606
+ this.resetAgentEndProbe(turn);
4607
+ if (this.deferAgentEndForSubsessions(turn, candidate)) return;
4608
+ }
4609
+ if (
4610
+ !turn.interrupted &&
4611
+ candidate.confidence !== "keyed" &&
4612
+ this.hasTerminalConflict(turn, false)
4613
+ ) {
4614
+ this.resetAgentEndProbe(turn);
4615
+ turn.deferredAgentEnd = candidate;
4616
+ this.pollUsage(turn);
4617
+ return;
4199
4618
  }
4200
4619
  if (state) {
4201
4620
  if (state.isStreaming || state.isCompacting) {
4202
- if (!turn.terminalOwnershipEvidence && !turn.terminalOwnershipRequired) {
4203
- const message = "OMP agent_end arrived while the native runtime remained active";
4204
- this.invalidateRuntime(message);
4205
- await this.finishTurn(turn, "failed", { message }, true, true);
4206
- return;
4207
- }
4208
- turn.agentEndPending = false;
4209
- turn.terminalizing = false;
4210
- turn.deferredAgentEnd = undefined;
4211
- return;
4212
- }
4213
- if (!turn.terminalOwnershipEvidence && turn.terminalOwnershipRequired) {
4214
- turn.agentEndPending = false;
4215
- turn.terminalizing = false;
4216
- turn.deferredAgentEnd = undefined;
4217
- this.scheduleTerminalOwnershipTimeout(turn);
4621
+ this.ignoreActiveTerminalCandidate(turn, candidate);
4218
4622
  return;
4219
4623
  }
4220
- await this.completeAgentEnd(turn, event);
4624
+ await this.completeAgentEnd(turn, candidate.event, true);
4221
4625
  return;
4222
4626
  }
4223
4627
  if (turn.userEchoObserved) {
@@ -4227,7 +4631,7 @@ export class OmpProviderSession {
4227
4631
  if (turn.agentEndRetryTimer === undefined) {
4228
4632
  turn.agentEndRetryTimer = this.scheduler.set(() => {
4229
4633
  turn.agentEndRetryTimer = undefined;
4230
- this.finishFromAgentEnd(turn, event);
4634
+ this.finishFromAgentEnd(turn, candidate);
4231
4635
  }, USAGE_POLL_MS);
4232
4636
  }
4233
4637
  }
@@ -4235,17 +4639,56 @@ export class OmpProviderSession {
4235
4639
  private async completeAgentEnd(
4236
4640
  turn: ActiveTurn,
4237
4641
  event: Extract<OmpRpcEvent, { type: "agent_end" }>,
4238
- usageSampled = false,
4642
+ providerIdle = false,
4239
4643
  ): Promise<void> {
4240
4644
  if (turn.terminal || this.activeTurn !== turn) return;
4241
- const error = terminalError(event, turn);
4242
- if (turn.interrupted) this.subsessions?.terminalize("canceled");
4243
- else if (error) this.subsessions?.terminalize("failed");
4244
- if (turn.interrupted) await this.finishTurn(turn, "canceled", undefined, usageSampled);
4245
- else if (error) await this.finishTurn(turn, "failed", { message: error }, usageSampled);
4246
- else await this.finishTurn(turn, "completed", undefined, usageSampled);
4645
+ let outcome = turn.interrupted ? ("canceled" as const) : terminalOutcome(event, turn);
4646
+ if (!outcome && providerIdle && event.messageCount !== undefined) {
4647
+ const runtime = this.runtime;
4648
+ const history = await this.readRuntimeHistoryWithTimeout(runtime);
4649
+ if (
4650
+ history &&
4651
+ history.length <= MAX_REPLAY_MESSAGES &&
4652
+ this.isCurrentRuntime(runtime, turn.generation) &&
4653
+ !turn.terminal &&
4654
+ !turn.interrupted &&
4655
+ this.activeTurn === turn
4656
+ ) {
4657
+ const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4658
+ if (
4659
+ turn.terminal ||
4660
+ this.activeTurn !== turn ||
4661
+ !this.isCurrentRuntime(runtime, turn.generation)
4662
+ )
4663
+ return;
4664
+ if (!turn.interrupted && state && (state.isStreaming || state.isCompacting)) {
4665
+ this.ignoreActiveTerminalCandidate(turn, {
4666
+ event,
4667
+ arrivalSequence: turn.activitySequence,
4668
+ confidence: event.requestId === undefined ? "ordered-legacy" : "keyed",
4669
+ });
4670
+ return;
4671
+ }
4672
+ if (state && !state.isStreaming && !state.isCompacting) {
4673
+ outcome = historyTerminalOutcome(history, event.messageCount, turn, event.messages ?? []);
4674
+ }
4675
+ }
4676
+ }
4677
+ if (turn.terminal || this.activeTurn !== turn) return;
4678
+ if (turn.interrupted || outcome === "canceled") {
4679
+ this.subsessions?.terminalize("canceled");
4680
+ await this.finishTurn(turn, "canceled");
4681
+ return;
4682
+ }
4683
+ if (outcome === "completed") {
4684
+ await this.finishTurn(turn, "completed");
4685
+ return;
4686
+ }
4687
+ const error =
4688
+ outcome === "failed" ? "OMP assistant turn failed" : unknownTerminalOutcomeError(event, turn);
4689
+ this.subsessions?.terminalize("failed");
4690
+ await this.finishTurn(turn, "failed", { message: error });
4247
4691
  }
4248
-
4249
4692
  private publishPendingUsers(turn: ActiveTurn): void {
4250
4693
  for (const pending of turn.pendingUsers.splice(0)) {
4251
4694
  for (const echo of pending.bufferedEchoes) {
@@ -4283,6 +4726,18 @@ export class OmpProviderSession {
4283
4726
  pending.bufferedEchoes.length = 0;
4284
4727
  }
4285
4728
 
4729
+ private settleUnstartedTurn(turn: ActiveTurn): void {
4730
+ if (turn.started || turn.terminal) return;
4731
+ turn.steerReady.resolve();
4732
+ turn.starting = false;
4733
+ turn.terminal = true;
4734
+ this.stopUsagePoll(turn);
4735
+ this.cancelAmbiguousTerminal(turn);
4736
+ this.resolveTurnPermissions(turn.turnId);
4737
+ this.projector.finishTurn(turn.turnId);
4738
+ if (this.activeTurn === turn) this.activeTurn = null;
4739
+ }
4740
+
4286
4741
  private publishPromptResult(
4287
4742
  turn: ActiveTurn,
4288
4743
  result: Extract<ProviderEvent, { type: "session.prompt_result" }>["result"],
@@ -4308,7 +4763,7 @@ export class OmpProviderSession {
4308
4763
  private finishTurn(
4309
4764
  turn: ActiveTurn,
4310
4765
  state: "completed" | "failed" | "canceled",
4311
- error?: { message: string },
4766
+ error?: ProviderError,
4312
4767
  usageSampled = false,
4313
4768
  override = false,
4314
4769
  preserveCompactions = false,
@@ -4332,7 +4787,7 @@ export class OmpProviderSession {
4332
4787
  turn.manualCompactionPending = false;
4333
4788
  turn.agentEndPending = false;
4334
4789
  this.cancelLocalOnlyCompletion(turn);
4335
- this.cancelTerminalOwnershipTimeout(turn);
4790
+ this.cancelAmbiguousTerminal(turn);
4336
4791
  this.stopUsagePoll(turn);
4337
4792
  if (turn.manualCompactionDeadlineTimer !== undefined) {
4338
4793
  this.scheduler.clear(turn.manualCompactionDeadlineTimer);