@omercnet/paseo-omp 0.3.0 → 0.4.0-next.114.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 (43) hide show
  1. package/README.md +19 -5
  2. package/client/omp-config-surface.tsx +243 -24
  3. package/client/omp-config-views.ts +24 -0
  4. package/client/omp-model-picker-state.ts +145 -0
  5. package/client/omp-model-picker.tsx +282 -0
  6. package/client/omp-routing-editor.tsx +307 -0
  7. package/client/support-diagnostics-state.ts +45 -0
  8. package/index.server.ts +27 -2
  9. package/package.json +2 -8
  10. package/paseo-plugin.json +1 -1
  11. package/server/omp-models.ts +59 -0
  12. package/server/omp-settings.ts +30 -20
  13. package/server/operational-failure-diagnostics.ts +76 -0
  14. package/server/package-version.ts +2 -0
  15. package/server/protocol-violation-diagnostics.ts +169 -0
  16. package/server/provider/catalog.ts +39 -10
  17. package/server/provider/connection.ts +60 -8
  18. package/server/provider/host-tools.ts +284 -34
  19. package/server/provider/mcp-transport.ts +2 -1
  20. package/server/provider/omp-rpc.ts +1011 -107
  21. package/server/provider/profile-providers.ts +7 -2
  22. package/server/provider/registration.ts +12 -2
  23. package/server/provider/security.ts +8 -10
  24. package/server/provider/session-descriptors.ts +45 -11
  25. package/server/provider/session.ts +200 -58
  26. package/server/provider/subsessions.ts +311 -73
  27. package/server/provider/timeline-projector.ts +34 -11
  28. package/server/support-diagnostics.ts +284 -0
  29. package/shared/omp-models.ts +49 -0
  30. package/shared/omp-settings.ts +227 -3
  31. package/shared/support-diagnostics.ts +32 -0
  32. package/CHANGELOG.md +0 -113
  33. package/SUPPORT.md +0 -44
  34. package/TESTING.md +0 -150
  35. package/docs/alpha-release-checklist.md +0 -68
  36. package/docs/configuration.md +0 -126
  37. package/docs/core-provider-issue-audit.md +0 -109
  38. package/docs/images/mcp-authorization-compact.png +0 -0
  39. package/docs/images/mcp-controls-wide.png +0 -0
  40. package/docs/images/plugin-manager.png +0 -0
  41. package/docs/images/workspace-settings.png +0 -0
  42. package/docs/installation.md +0 -89
  43. package/tsconfig.json +0 -16
@@ -11,7 +11,19 @@ import type {
11
11
  ProviderUsage,
12
12
  } from "@getpaseo/plugin/server/provider";
13
13
  import { getForgeDefinitionOrNeutral } from "@getpaseo/protocol/forge-manifest";
14
- import { mapOmpModels, nativeOmpModelId, OMP_MODES, ompModelId, thinkingForModel } from "./catalog";
14
+ import type {
15
+ OmpOperationalFailure,
16
+ OmpOperationalFailureReporter,
17
+ } from "../operational-failure-diagnostics";
18
+ import {
19
+ mapOmpModels,
20
+ nativeOmpModelId,
21
+ OMP_MODES,
22
+ ompModelId,
23
+ selectOmpModels,
24
+ thinkingForModel,
25
+ validateOmpModelIdentities,
26
+ } from "./catalog";
15
27
  import {
16
28
  normalizeOmpSessionConfig,
17
29
  type OmpRecoveryOptions,
@@ -21,6 +33,7 @@ import { OmpHostToolsBridge, type OmpMcpConnector, validateOmpHostToolConfig } f
21
33
  import { isOmpImageMimeType, isValidImagePayload, OmpImageMaterializer } from "./image";
22
34
  import type {
23
35
  OmpAvailableCommand,
36
+ OmpBranchResult,
24
37
  OmpCompactionResult,
25
38
  OmpExtensionUiResponse,
26
39
  OmpImage,
@@ -35,7 +48,7 @@ import type {
35
48
  OmpToolApprovalCancel,
36
49
  OmpToolApprovalRequest,
37
50
  } from "./omp-rpc";
38
- import { buildOmpSpawnRequest } from "./omp-rpc";
51
+ import { buildOmpSpawnRequest, OmpRpcRequestRejectedError } from "./omp-rpc";
39
52
  import {
40
53
  BoundedStringSet,
41
54
  boundedJsonBytes,
@@ -283,6 +296,7 @@ type ActiveTurn = {
283
296
  terminalizing: boolean;
284
297
  terminalization?: Promise<void>;
285
298
  terminalOutcome?: TurnOutcome;
299
+ operationalTerminalStage?: "failed" | "unresolved";
286
300
  terminalWake?: VoidDeferred;
287
301
  steerReady: VoidDeferred;
288
302
  steersInFlight: number;
@@ -938,6 +952,8 @@ export class OmpProviderSession {
938
952
  private readonly pendingToolPermissions = new Map<string, PendingToolPermission>();
939
953
  private readonly inFlightToolPermissions = new Map<string, PendingToolPermission>();
940
954
  private readonly resolvedToolApprovalIds = new BoundedStringSet(MAX_TRACKED_ENTRY_IDS);
955
+ private unsupportedThinkingNoticePending = false;
956
+ private unsupportedThinkingNoticePublished = false;
941
957
 
942
958
  private constructor(
943
959
  id: string,
@@ -962,6 +978,7 @@ export class OmpProviderSession {
962
978
  private readonly transitionNativeSession: NativeSessionTransition,
963
979
  private readonly quarantineRewindCleanup: RewindCleanupQuarantine,
964
980
  private readonly retireRewindSession: RewindSessionRetirement,
981
+ private readonly reportOperationalFailure: OmpOperationalFailureReporter,
965
982
  scheduler: OmpTimelineScheduler = defaultOmpTimelineScheduler,
966
983
  ) {
967
984
  this.id = id;
@@ -995,6 +1012,13 @@ export class OmpProviderSession {
995
1012
  : null;
996
1013
  this.bindRuntime(runtime);
997
1014
  }
1015
+ private recordOperationalFailure(failure: OmpOperationalFailure): void {
1016
+ try {
1017
+ this.reportOperationalFailure(failure);
1018
+ } catch {
1019
+ // Diagnostics must never affect provider or session flow.
1020
+ }
1021
+ }
998
1022
  get persistenceSessionId(): string | undefined {
999
1023
  return this.persistSession ? this.nativeSessionId : undefined;
1000
1024
  }
@@ -1021,6 +1045,7 @@ export class OmpProviderSession {
1021
1045
  environment?: NodeJS.ProcessEnv,
1022
1046
  mcpConnector?: OmpMcpConnector,
1023
1047
  mcpInitializationTimeoutMs?: number,
1048
+ reportOperationalFailure: OmpOperationalFailureReporter = () => {},
1024
1049
  ): Promise<OmpProviderSession> {
1025
1050
  const resumeSessionId = ompPersistenceSessionId(input);
1026
1051
  if (resumeSessionId && !input.config.persist) {
@@ -1056,10 +1081,9 @@ export class OmpProviderSession {
1056
1081
  }
1057
1082
  const startOptions: OmpStartOptions = {
1058
1083
  ...normalizedConfig,
1059
- // Model selection is authorized only after this runtime reports its exact catalog.
1060
- ...(resumeSessionId
1061
- ? { thinkingOption: undefined, systemPrompt: undefined, resumeSessionId }
1062
- : {}),
1084
+ // Thinking is authorized only after this runtime reports its exact model catalog.
1085
+ thinkingOption: undefined,
1086
+ ...(resumeSessionId ? { systemPrompt: undefined, resumeSessionId } : {}),
1063
1087
  signal,
1064
1088
  environment,
1065
1089
  };
@@ -1068,6 +1092,7 @@ export class OmpProviderSession {
1068
1092
  connectMcp: mcpConnector,
1069
1093
  signal,
1070
1094
  initializationTimeoutMs: mcpInitializationTimeoutMs,
1095
+ reportOperationalFailure,
1071
1096
  });
1072
1097
  let native: OmpRuntimeSession | undefined;
1073
1098
  let cleanupNativeSessionId: string | undefined;
@@ -1095,6 +1120,7 @@ export class OmpProviderSession {
1095
1120
  () => ({ available: false, commands: [] }),
1096
1121
  ),
1097
1122
  ]);
1123
+ validateOmpModelIdentities(nativeModels);
1098
1124
  if (effectiveConfig.persist && !native.canReplayHistory) {
1099
1125
  throw new OmpPublicError("OMP session persistence requires negotiated RPC protocol v2");
1100
1126
  }
@@ -1108,12 +1134,10 @@ export class OmpProviderSession {
1108
1134
  if (resumeSessionId && initialState.sessionId !== resumeSessionId) {
1109
1135
  throw new OmpPublicError("OMP resumed a different native session");
1110
1136
  }
1111
- const models = mapOmpModels(nativeModels, new OmpPublicDataSerializer(outputRedactionValues));
1112
- const nativeModelsByPublicId = new Map(
1113
- nativeModels.map((model) => [ompModelId(model), model] as const),
1114
- );
1115
1137
  if (!resumeSessionId && input.config.model) {
1116
- const selected = nativeModelsByPublicId.get(input.config.model);
1138
+ const selected = selectOmpModels(nativeModels, state.model).find(
1139
+ (model) => ompModelId(model) === input.config.model,
1140
+ );
1117
1141
  if (!selected) {
1118
1142
  throw new OmpPublicError("OMP model is not advertised by the configured session runtime");
1119
1143
  }
@@ -1122,9 +1146,29 @@ export class OmpProviderSession {
1122
1146
  state = await native.getState();
1123
1147
  }
1124
1148
  }
1149
+ let currentModel = state.model
1150
+ ? nativeModels.find(
1151
+ (model) => model.provider === state.model?.provider && model.id === state.model.id,
1152
+ )
1153
+ : undefined;
1154
+ if (state.model && !currentModel) {
1155
+ throw new OmpPublicError("OMP runtime selected an unadvertised model");
1156
+ }
1157
+ if (!resumeSessionId && input.config.thinkingOption !== undefined) {
1158
+ if (
1159
+ !thinkingForModel(currentModel).some(
1160
+ (option) => option.id === input.config.thinkingOption,
1161
+ )
1162
+ ) {
1163
+ throw new OmpPublicError("OMP thinking level is unavailable for the selected model");
1164
+ }
1165
+ if (state.thinkingLevel !== input.config.thinkingOption) {
1166
+ await native.setThinkingLevel(input.config.thinkingOption);
1167
+ }
1168
+ }
1125
1169
  const reconciledConfigRevision = bootstrapConfigRevision;
1126
1170
  state = await native.getState();
1127
- const currentModel = state.model
1171
+ currentModel = state.model
1128
1172
  ? nativeModels.find(
1129
1173
  (model) => model.provider === state.model?.provider && model.id === state.model.id,
1130
1174
  )
@@ -1132,21 +1176,21 @@ export class OmpProviderSession {
1132
1176
  if (state.model && !currentModel) {
1133
1177
  throw new OmpPublicError("OMP runtime selected an unadvertised model");
1134
1178
  }
1179
+ const selectedNativeModels = selectOmpModels(nativeModels, state.model);
1180
+ const models = mapOmpModels(
1181
+ selectedNativeModels,
1182
+ new OmpPublicDataSerializer(outputRedactionValues),
1183
+ );
1184
+ const nativeModelsByPublicId = new Map(
1185
+ nativeModels.map((model) => [ompModelId(model), model] as const),
1186
+ );
1135
1187
  const thinkingOptions = thinkingForModel(currentModel);
1136
- const committedThinkingLevel = applicableThinkingLevel(currentModel, state.thinkingLevel);
1137
- if (
1138
- !resumeSessionId &&
1139
- input.config.thinkingOption !== undefined &&
1140
- !thinkingOptions.some((option) => option.id === input.config.thinkingOption)
1141
- ) {
1142
- throw new OmpPublicError("OMP thinking level is unavailable for the selected model");
1143
- }
1144
- if (
1145
- committedThinkingLevel &&
1146
- !thinkingOptions.some((option) => option.id === committedThinkingLevel)
1147
- ) {
1148
- throw new OmpPublicError("OMP runtime selected an unsupported thinking level");
1149
- }
1188
+ const applicableLevel = applicableThinkingLevel(currentModel, state.thinkingLevel);
1189
+ const committedThinkingLevel = thinkingOptions.some((option) => option.id === applicableLevel)
1190
+ ? applicableLevel
1191
+ : undefined;
1192
+ const unsupportedThinkingLevel =
1193
+ applicableLevel !== undefined && committedThinkingLevel === undefined;
1150
1194
  const configState: ProviderConfigState = {
1151
1195
  ...(state.model ? { model: ompModelId(state.model) } : {}),
1152
1196
  mode: normalizedConfig.mode ?? "full",
@@ -1207,8 +1251,10 @@ export class OmpProviderSession {
1207
1251
  transitionNativeSession,
1208
1252
  quarantineRewindCleanup,
1209
1253
  retireRewindSession,
1254
+ reportOperationalFailure,
1210
1255
  scheduler,
1211
1256
  );
1257
+ session.unsupportedThinkingNoticePending = unsupportedThinkingLevel;
1212
1258
 
1213
1259
  if (bootstrapConfigRevision !== reconciledConfigRevision) {
1214
1260
  session.configRefreshDirty = true;
@@ -1267,6 +1313,7 @@ export class OmpProviderSession {
1267
1313
  : {}),
1268
1314
  ...(this.config.title ? { title: this.dataFilter.text(this.config.title, 256) } : {}),
1269
1315
  });
1316
+ this.publishUnsupportedThinkingNotice();
1270
1317
  this.emit({ type: "session.config", sessionId: this.id, config: this.configState });
1271
1318
  if (this.replayHistoryOnOpen) await this.replayHistory(true);
1272
1319
  this.publishCommands(this.commandCatalog);
@@ -1274,6 +1321,22 @@ export class OmpProviderSession {
1274
1321
  this.readyPublished = true;
1275
1322
  if (this.configRefreshDirty) this.scheduleCommittedConfigRefresh();
1276
1323
  }
1324
+ private publishUnsupportedThinkingNotice(): void {
1325
+ if (!this.unsupportedThinkingNoticePending || this.unsupportedThinkingNoticePublished) return;
1326
+ this.unsupportedThinkingNoticePending = false;
1327
+ this.unsupportedThinkingNoticePublished = true;
1328
+ this.emit({
1329
+ type: "session.notice",
1330
+ sessionId: this.id,
1331
+ notice: {
1332
+ id: "omp:unsupported-thinking-level",
1333
+ severity: "warning",
1334
+ title: "OMP thinking level unavailable",
1335
+ description:
1336
+ "OMP reported a thinking level that the active model does not advertise. Paseo omitted it from the session configuration.",
1337
+ },
1338
+ });
1339
+ }
1277
1340
 
1278
1341
  private usageFrom(
1279
1342
  state: OmpSessionState | undefined,
@@ -1592,8 +1655,24 @@ export class OmpProviderSession {
1592
1655
  replay.signal,
1593
1656
  );
1594
1657
  messages = transcript.messages;
1658
+ if (transcript.imageReplayWarning) {
1659
+ this.emit({
1660
+ type: "timeline.item",
1661
+ sessionId: this.id,
1662
+ item: {
1663
+ id: "omp:replay-image-unavailable",
1664
+ type: "notification",
1665
+ level: "warning",
1666
+ message: "OMP skipped one or more unavailable images while replaying this session.",
1667
+ },
1668
+ });
1669
+ }
1595
1670
  } catch (error) {
1596
1671
  if (replay.signal.aborted) throw error;
1672
+ this.recordOperationalFailure({
1673
+ category: "replay-recovery",
1674
+ stage: "persisted-replay",
1675
+ });
1597
1676
  this.emit({
1598
1677
  type: "timeline.item",
1599
1678
  sessionId: this.id,
@@ -1667,19 +1746,33 @@ export class OmpProviderSession {
1667
1746
  throw new OmpPublicError("OMP conversation rewind requires negotiated RPC protocol v2");
1668
1747
  }
1669
1748
  const entryId = this.projector.resolveRevertToken(input.token);
1670
- const [branchMessages, beforeState] = await Promise.all([
1671
- runtime.getBranchMessages(),
1672
- runtime.getState(),
1673
- ]);
1749
+ const beforeState = await runtime.getState();
1674
1750
  this.requireCurrentRuntime(runtime, generation);
1675
1751
  if (this.activeTurn || beforeState.isStreaming || beforeState.isCompacting) {
1676
1752
  throw new OmpPublicError("Cannot rewind the OMP conversation while a turn is active");
1677
1753
  }
1678
- if (!branchMessages.some((message) => message.entryId === entryId)) {
1679
- throw new OmpPublicError("OMP conversation rewind token is stale");
1754
+ const beforeAdvertisedModel = beforeState.model
1755
+ ? this.nativeModelsByPublicId.get(ompModelId(beforeState.model))
1756
+ : undefined;
1757
+ if (beforeState.model && !beforeAdvertisedModel) {
1758
+ throw new OmpPublicError("OMP runtime selected an unadvertised model");
1680
1759
  }
1760
+ const restorableThinkingLevel = thinkingForModel(beforeAdvertisedModel).some(
1761
+ (option) => option.id === beforeState.thinkingLevel,
1762
+ )
1763
+ ? beforeState.thinkingLevel
1764
+ : undefined;
1681
1765
  branchMutationPossible = true;
1682
- const result = await runtime.branch(entryId);
1766
+ let result: OmpBranchResult;
1767
+ try {
1768
+ result = await runtime.branch(entryId);
1769
+ } catch (error) {
1770
+ if (error instanceof OmpRpcRequestRejectedError) {
1771
+ branchMutationPossible = false;
1772
+ throw new OmpPublicError("OMP conversation rewind token is stale");
1773
+ }
1774
+ throw error;
1775
+ }
1683
1776
  this.requireCurrentRuntime(runtime, generation);
1684
1777
  if (result.cancelled) {
1685
1778
  branchMutationPossible = false;
@@ -1711,13 +1804,10 @@ export class OmpProviderSession {
1711
1804
  }
1712
1805
  await runtime.setModel(beforeState.model.provider, beforeState.model.id);
1713
1806
  }
1714
- if (thinkingChanged) {
1715
- if (!beforeState.thinkingLevel) {
1716
- throw new OmpPublicError("OMP changed thinking level while rewinding the conversation");
1717
- }
1718
- await runtime.setThinkingLevel(beforeState.thinkingLevel);
1807
+ if (thinkingChanged && restorableThinkingLevel) {
1808
+ await runtime.setThinkingLevel(restorableThinkingLevel);
1719
1809
  }
1720
- if (modelChanged || thinkingChanged) {
1810
+ if (modelChanged || (thinkingChanged && restorableThinkingLevel)) {
1721
1811
  state = await runtime.getState();
1722
1812
  this.requireCurrentRuntime(runtime, generation);
1723
1813
  }
@@ -1725,7 +1815,7 @@ export class OmpProviderSession {
1725
1815
  state.sessionId !== this.nativeSessionId ||
1726
1816
  state.model?.provider !== beforeState.model?.provider ||
1727
1817
  state.model?.id !== beforeState.model?.id ||
1728
- state.thinkingLevel !== beforeState.thinkingLevel
1818
+ (restorableThinkingLevel !== undefined && state.thinkingLevel !== restorableThinkingLevel)
1729
1819
  ) {
1730
1820
  throw new OmpPublicError("OMP did not preserve session configuration while rewinding");
1731
1821
  }
@@ -1737,6 +1827,7 @@ export class OmpProviderSession {
1737
1827
  this.publishCommittedConfig(state, runtime, generation);
1738
1828
  this.emit({ type: "request.completed", requestId: input.requestId });
1739
1829
  } catch (error) {
1830
+ this.recordOperationalFailure({ category: "replay-recovery", stage: "rewind" });
1740
1831
  const failure = branchMutationPossible
1741
1832
  ? { message: "OMP conversation rewind left native state indeterminate" }
1742
1833
  : providerError(error, "OMP conversation rewind failed");
@@ -2325,7 +2416,10 @@ export class OmpProviderSession {
2325
2416
  const targetModel = targetModelId
2326
2417
  ? this.nativeModelsByPublicId.get(targetModelId)
2327
2418
  : undefined;
2328
- if (input.changes.model !== undefined && !targetModel) {
2419
+ const targetModelPublished = this.configState.models.some(
2420
+ (model) => model.id === targetModelId,
2421
+ );
2422
+ if (input.changes.model !== undefined && (!targetModel || !targetModelPublished)) {
2329
2423
  throw new OmpPublicError("OMP model selection is unavailable");
2330
2424
  }
2331
2425
  if (
@@ -2572,13 +2666,18 @@ export class OmpProviderSession {
2572
2666
  throw new OmpCatalogEscape("OMP runtime selected an unadvertised model");
2573
2667
  }
2574
2668
  const thinkingOptions = thinkingForModel(advertisedModel);
2575
- const committedThinkingLevel = applicableThinkingLevel(advertisedModel, state.thinkingLevel);
2576
- if (
2577
- committedThinkingLevel &&
2578
- !thinkingOptions.some((option) => option.id === committedThinkingLevel)
2579
- ) {
2580
- throw new OmpCatalogEscape("OMP runtime selected an unsupported thinking level");
2669
+ const applicableLevel = applicableThinkingLevel(advertisedModel, state.thinkingLevel);
2670
+ const committedThinkingLevel = thinkingOptions.some((option) => option.id === applicableLevel)
2671
+ ? applicableLevel
2672
+ : undefined;
2673
+ if (applicableLevel !== undefined && committedThinkingLevel === undefined) {
2674
+ this.unsupportedThinkingNoticePending = true;
2675
+ this.publishUnsupportedThinkingNotice();
2581
2676
  }
2677
+ const models = mapOmpModels(
2678
+ selectOmpModels([...this.nativeModelsByPublicId.values()], state.model),
2679
+ this.dataFilter,
2680
+ );
2582
2681
  this.configRefreshAttempts = 0;
2583
2682
  this.cancelConfigRefreshRetry();
2584
2683
  const nextConfig: ProviderConfigState = {
@@ -2587,12 +2686,15 @@ export class OmpProviderSession {
2587
2686
  ...(committedThinkingLevel
2588
2687
  ? { thinkingOption: committedThinkingLevel }
2589
2688
  : { thinkingOption: undefined }),
2689
+ models,
2590
2690
  thinkingOptions,
2591
2691
  };
2592
2692
  const changed =
2593
2693
  force ||
2594
2694
  nextConfig.model !== this.configState.model ||
2595
2695
  nextConfig.thinkingOption !== this.configState.thinkingOption ||
2696
+ nextConfig.models.some((model, index) => model.id !== this.configState.models[index]?.id) ||
2697
+ this.configState.models.length !== nextConfig.models.length ||
2596
2698
  nextConfig.thinkingOptions.length !== this.configState.thinkingOptions.length ||
2597
2699
  nextConfig.thinkingOptions.some(
2598
2700
  (option, index) =>
@@ -2843,7 +2945,13 @@ export class OmpProviderSession {
2843
2945
 
2844
2946
  private async recoverRuntime(): Promise<void> {
2845
2947
  if (!this.runtimeDead) return;
2846
- this.recoveryPromise ??= this.startRecovery();
2948
+ this.recoveryPromise ??= this.startRecovery().catch((error) => {
2949
+ this.recordOperationalFailure({
2950
+ category: "replay-recovery",
2951
+ stage: "runtime-recovery",
2952
+ });
2953
+ throw error;
2954
+ });
2847
2955
  try {
2848
2956
  await this.recoveryPromise;
2849
2957
  } finally {
@@ -2913,13 +3021,6 @@ export class OmpProviderSession {
2913
3021
  if (state.model && !advertisedModel) {
2914
3022
  throw new Error("OMP recovered with an unadvertised model");
2915
3023
  }
2916
- const recoveredThinkingLevel = applicableThinkingLevel(advertisedModel, state.thinkingLevel);
2917
- if (
2918
- recoveredThinkingLevel &&
2919
- !thinkingForModel(advertisedModel).some((option) => option.id === recoveredThinkingLevel)
2920
- ) {
2921
- throw new Error("OMP recovered with an unsupported thinking level");
2922
- }
2923
3024
  if (this.closed) throw new Error("OMP session closed while runtime recovery was pending");
2924
3025
  if (!this.hostTools.isBoundTo(recovered)) {
2925
3026
  throw new Error("OMP host tool bridge detached during recovery");
@@ -3073,6 +3174,10 @@ export class OmpProviderSession {
3073
3174
  try {
3074
3175
  this.subsessions?.handle(event);
3075
3176
  } catch {
3177
+ this.recordOperationalFailure({
3178
+ category: "tool-projector",
3179
+ stage: "subsession-projector",
3180
+ });
3076
3181
  this.handleRuntimeFailure("OMP subagent event processing failed");
3077
3182
  }
3078
3183
  return;
@@ -3307,6 +3412,10 @@ export class OmpProviderSession {
3307
3412
  try {
3308
3413
  this.subsessions?.observeSessionEvent(this.id, event);
3309
3414
  } catch {
3415
+ this.recordOperationalFailure({
3416
+ category: "tool-projector",
3417
+ stage: "subsession-projector",
3418
+ });
3310
3419
  this.handleRuntimeFailure("OMP subagent dispatch tracking failed");
3311
3420
  return;
3312
3421
  }
@@ -3363,7 +3472,15 @@ export class OmpProviderSession {
3363
3472
  }
3364
3473
  }
3365
3474
  if (isNativeTurnActivity(event)) this.markAgentEvidence(turn);
3366
- this.projector.project(event, turn.turnId);
3475
+ try {
3476
+ this.projector.project(event, turn.turnId);
3477
+ } catch (error) {
3478
+ this.recordOperationalFailure({
3479
+ category: "tool-projector",
3480
+ stage: "timeline-projector",
3481
+ });
3482
+ throw error;
3483
+ }
3367
3484
  if (event.type === "tool_execution_end") this.resumeDeferredAgentEnd();
3368
3485
  }
3369
3486
 
@@ -4432,11 +4549,13 @@ export class OmpProviderSession {
4432
4549
  const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4433
4550
  if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
4434
4551
  if (!state) {
4552
+ turn.operationalTerminalStage = "unresolved";
4435
4553
  this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
4436
4554
  return;
4437
4555
  }
4438
4556
  turn.deferredAgentEnd = undefined;
4439
4557
  if (state.isStreaming || state.isCompacting) return;
4558
+ turn.operationalTerminalStage = "unresolved";
4440
4559
  await this.finishTurn(turn, "failed", {
4441
4560
  message: "OMP unkeyed agent_end could not be correlated to the current prompt",
4442
4561
  });
@@ -4517,6 +4636,7 @@ export class OmpProviderSession {
4517
4636
  ) {
4518
4637
  void this.completeAgentEnd(turn, candidate.event);
4519
4638
  } else {
4639
+ turn.operationalTerminalStage = "unresolved";
4520
4640
  this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
4521
4641
  }
4522
4642
  }, AGENT_END_SETTLE_MS);
@@ -4541,8 +4661,13 @@ export class OmpProviderSession {
4541
4661
  turn.terminalizing = false;
4542
4662
  turn.deferredAgentEnd = candidate;
4543
4663
  void this.subsessions.reconcile(this.runtime).catch(() => {
4664
+ this.recordOperationalFailure({
4665
+ category: "tool-projector",
4666
+ stage: "subsession-projector",
4667
+ });
4544
4668
  if (!turn.terminal && this.activeTurn === turn) {
4545
- this.handleRuntimeFailure("OMP subagent reconciliation failed");
4669
+ this.subsessions?.terminalize("failed");
4670
+ this.resumeDeferredAgentEnd();
4546
4671
  }
4547
4672
  });
4548
4673
  return true;
@@ -4625,6 +4750,7 @@ export class OmpProviderSession {
4625
4750
  return;
4626
4751
  }
4627
4752
  if (turn.userEchoObserved) {
4753
+ turn.operationalTerminalStage = "unresolved";
4628
4754
  this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
4629
4755
  return;
4630
4756
  }
@@ -4684,6 +4810,7 @@ export class OmpProviderSession {
4684
4810
  await this.finishTurn(turn, "completed");
4685
4811
  return;
4686
4812
  }
4813
+ turn.operationalTerminalStage = outcome === "failed" ? "failed" : "unresolved";
4687
4814
  const error =
4688
4815
  outcome === "failed" ? "OMP assistant turn failed" : unknownTerminalOutcomeError(event, turn);
4689
4816
  this.subsessions?.terminalize("failed");
@@ -4826,6 +4953,12 @@ export class OmpProviderSession {
4826
4953
  usageSampled: true,
4827
4954
  };
4828
4955
  }
4956
+ if (outcome.state === "failed") {
4957
+ this.recordOperationalFailure({
4958
+ category: "terminal-outcome",
4959
+ stage: turn.operationalTerminalStage ?? "failed",
4960
+ });
4961
+ }
4829
4962
  this.imageMaterializer.clear();
4830
4963
  turn.terminal = true;
4831
4964
  if (this.usageSample?.turn === turn) this.usageSample = null;
@@ -4839,7 +4972,15 @@ export class OmpProviderSession {
4839
4972
  }
4840
4973
  this.publishPendingUsers(turn);
4841
4974
  this.resolveTurnPermissions(turn.turnId);
4842
- this.projector.finishTurn(turn.turnId, preserveCompactions);
4975
+ try {
4976
+ this.projector.finishTurn(turn.turnId, preserveCompactions);
4977
+ } catch (error) {
4978
+ this.recordOperationalFailure({
4979
+ category: "tool-projector",
4980
+ stage: "timeline-projector",
4981
+ });
4982
+ throw error;
4983
+ }
4843
4984
  this.unclaimedBranchEntries.length = 0;
4844
4985
  this.emit({
4845
4986
  type: "session.turn",
@@ -4897,6 +5038,7 @@ export class OmpProviderSession {
4897
5038
  this.publishPromptResult(turn, { type: "failed", error: { message } });
4898
5039
  if (turn.started) void this.finishTurn(turn, "failed", { message }, true, true);
4899
5040
  else {
5041
+ this.recordOperationalFailure({ category: "terminal-outcome", stage: "failed" });
4900
5042
  turn.steerReady.resolve();
4901
5043
  turn.terminal = true;
4902
5044
  this.projector.finishTurn(turn.turnId);