@omercnet/paseo-omp 0.3.0-next.102.1 → 0.3.0-next.104.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "0.3.0-next.102.1",
3
+ "version": "0.3.0-next.104.1",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -48,6 +48,27 @@ const THINKING_OPTIONS: readonly ProviderThinkingOption[] = [
48
48
  { id: "xhigh", label: "XHigh", description: "Extra-high reasoning" },
49
49
  { id: "max", label: "Max", description: "Maximum reasoning" },
50
50
  ];
51
+ export const OMP_MAX_CATALOG_MODELS = 256;
52
+
53
+ export function selectOmpModels(
54
+ models: readonly OmpModel[],
55
+ activeModel: OmpModel | null | undefined,
56
+ ): OmpModel[] {
57
+ if (models.length <= OMP_MAX_CATALOG_MODELS) return [...models];
58
+ const selected = models.slice(0, OMP_MAX_CATALOG_MODELS);
59
+ if (!activeModel) return selected;
60
+ const active = models.find(
61
+ (model) => model.provider === activeModel.provider && model.id === activeModel.id,
62
+ );
63
+ if (
64
+ !active ||
65
+ selected.some((model) => model.provider === active.provider && model.id === active.id)
66
+ ) {
67
+ return selected;
68
+ }
69
+ selected[OMP_MAX_CATALOG_MODELS - 1] = active;
70
+ return selected;
71
+ }
51
72
 
52
73
  export function nativeOmpModelId(model: OmpModel): string {
53
74
  if (model.provider.includes("/")) {
@@ -60,14 +81,9 @@ export function ompModelId(model: OmpModel): string {
60
81
  const nativeIdentity = `${Buffer.byteLength(model.provider, "utf8")}:${model.provider}${Buffer.byteLength(model.id, "utf8")}:${model.id}`;
61
82
  return `omp:model:${createHash("sha256").update(nativeIdentity).digest("hex")}`;
62
83
  }
63
-
64
- export function mapOmpModels(
65
- models: readonly OmpModel[],
66
- serializer = new OmpPublicDataSerializer(),
67
- ): ProviderModel[] {
84
+ export function validateOmpModelIdentities(models: readonly OmpModel[]): void {
68
85
  const seenIds = new Map<string, string>();
69
- return models.map((model) => {
70
- const thinkingOptions = thinkingForModel(model);
86
+ for (const model of models) {
71
87
  const id = ompModelId(model);
72
88
  const nativeIdentity = nativeOmpModelId(model);
73
89
  const existing = seenIds.get(id);
@@ -76,6 +92,17 @@ export function mapOmpModels(
76
92
  }
77
93
  if (existing !== undefined) throw new Error("OMP reported a duplicate model identity");
78
94
  seenIds.set(id, nativeIdentity);
95
+ }
96
+ }
97
+
98
+ export function mapOmpModels(
99
+ models: readonly OmpModel[],
100
+ serializer = new OmpPublicDataSerializer(),
101
+ ): ProviderModel[] {
102
+ validateOmpModelIdentities(models);
103
+ return models.map((model) => {
104
+ const thinkingOptions = thinkingForModel(model);
105
+ const id = ompModelId(model);
79
106
  const provider = serializer.text(model.provider, 256);
80
107
  const modelId = serializer.text(model.id, 256);
81
108
  const name = model.name ? serializer.text(model.name, 256) : modelId;
@@ -143,14 +170,16 @@ export async function discoverOmpCatalog(
143
170
  ? [...configuredValues, ...(session.inheritedRedactionValues ?? [])]
144
171
  : configuredValues,
145
172
  );
146
- const models = mapOmpModels(nativeModels, serializer);
173
+ validateOmpModelIdentities(nativeModels);
174
+ const selectedNativeModels = selectOmpModels(nativeModels, state.model);
175
+ const models = mapOmpModels(selectedNativeModels, serializer);
147
176
  if (models.length === 0) throw new Error("OMP reported no available models");
148
177
  const defaultModel = state.model ? ompModelId(state.model) : models[0]?.id;
149
178
  const currentModel = state.model
150
- ? nativeModels.find(
179
+ ? selectedNativeModels.find(
151
180
  (model) => model.provider === state.model?.provider && model.id === state.model.id,
152
181
  )
153
- : nativeModels[0];
182
+ : selectedNativeModels[0];
154
183
  if (state.model && !currentModel) throw new Error("OMP reported an unadvertised active model");
155
184
  const thinkingOptions = thinkingForModel(currentModel);
156
185
  const defaultThinkingOption = thinkingOptions.some(
@@ -70,6 +70,7 @@ const MAX_OPTIONAL_METADATA_NODES = 4_096;
70
70
  const MAX_TASK_CORRELATION_BYTES = 256 * 1024;
71
71
  const MAX_TASK_CORRELATION_ITEMS = 1_024;
72
72
  const MAX_TASK_CORRELATION_NODES = 4_096;
73
+ const MAX_MODEL_CATALOG_ITEMS = 4_096;
73
74
  // Tool-intensive OMP turns legitimately exceed 64 blocks; transport byte/node budgets remain the
74
75
  // primary resource bounds.
75
76
  export const OMP_MAX_CONTENT_PARTS = 4_096;
@@ -115,6 +116,17 @@ function boundedString(maxBytes: number, minBytes = 0) {
115
116
  return bytes >= minBytes && bytes <= maxBytes;
116
117
  });
117
118
  }
119
+ function boundedOptionalStringArray(maxItems: number, maxBytes: number) {
120
+ return z.unknown().transform((value): string[] | undefined => {
121
+ if (!Array.isArray(value)) return undefined;
122
+ return value
123
+ .filter(
124
+ (item): item is string =>
125
+ typeof item === "string" && item.length > 0 && utf8Bytes(item) <= maxBytes,
126
+ )
127
+ .slice(0, maxItems);
128
+ });
129
+ }
118
130
 
119
131
  const IDENTIFIER = boundedString(MAX_ID_LENGTH, 1);
120
132
  const NAME = boundedString(MAX_NAME_LENGTH, 1);
@@ -191,6 +203,7 @@ function sanitizeLiveDisplayFrame(value: unknown): unknown {
191
203
  return changed ? { ...frame, messages } : value;
192
204
  }
193
205
  const OmpThinkingLevelSchema = z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
206
+ const OmpCurrentThinkingLevelSchema = boundedString(32).optional().catch(undefined);
194
207
 
195
208
  function isBoundedJson(
196
209
  value: unknown,
@@ -546,19 +559,33 @@ const OmpAvailableCommandSchema = z.object({
546
559
  .optional(),
547
560
  source: boundedString(64).optional(),
548
561
  });
562
+ const OmpThinkingMetadataSchema = z
563
+ .unknown()
564
+ .transform((value): { efforts?: string[]; defaultLevel?: string } | undefined => {
565
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
566
+ const record = value as Record<string, unknown>;
567
+ const efforts = boundedOptionalStringArray(16, 32).parse(record.efforts);
568
+ const defaultLevel = boundedString(32).optional().catch(undefined).parse(record.defaultLevel);
569
+ return {
570
+ ...(efforts !== undefined ? { efforts } : {}),
571
+ ...(defaultLevel !== undefined ? { defaultLevel } : {}),
572
+ };
573
+ });
549
574
  const OmpModelSchema = z.object({
550
575
  provider: OMP_PROVIDER_NAME,
551
576
  id: NAME,
552
- name: boundedString(MAX_NAME_LENGTH).optional(),
553
- reasoning: z.boolean().optional(),
554
- thinking: z
555
- .object({
556
- efforts: z.array(boundedString(32)).max(16).optional(),
557
- defaultLevel: boundedString(32).optional(),
558
- })
559
- .optional(),
560
- input: z.array(NAME).max(16).optional(),
561
- contextWindow: z.number().int().nonnegative().max(100_000_000).nullable().optional(),
577
+ name: boundedString(MAX_NAME_LENGTH).optional().catch(undefined),
578
+ reasoning: z.boolean().optional().catch(undefined),
579
+ thinking: OmpThinkingMetadataSchema.optional(),
580
+ input: boundedOptionalStringArray(16, MAX_NAME_LENGTH).optional(),
581
+ contextWindow: z
582
+ .number()
583
+ .int()
584
+ .nonnegative()
585
+ .max(100_000_000)
586
+ .nullable()
587
+ .optional()
588
+ .catch(undefined),
562
589
  });
563
590
  const TokenCountSchema = z.number().int().nonnegative().max(MAX_TOKEN_COUNT);
564
591
  const OptionalTokenCountSchema = TokenCountSchema.nullable().optional();
@@ -610,13 +637,13 @@ const OmpCompactionResultSchema = z.object({
610
637
  });
611
638
  const OmpSessionStateSchema = z.object({
612
639
  model: OmpModelSchema.nullable().optional(),
613
- thinkingLevel: OmpThinkingLevelSchema.optional(),
640
+ thinkingLevel: OmpCurrentThinkingLevelSchema,
614
641
  isStreaming: z.boolean(),
615
642
  isCompacting: z.boolean(),
616
643
  sessionId: IDENTIFIER,
617
644
  autoCompactionEnabled: z.boolean().optional(),
618
- contextUsage: OmpContextUsageSchema.nullable().optional(),
619
- sessionFile: boundedString(MAX_PATH_LENGTH).optional(),
645
+ contextUsage: OmpContextUsageSchema.nullable().optional().catch(undefined),
646
+ sessionFile: boundedString(MAX_PATH_LENGTH).optional().catch(undefined),
620
647
  });
621
648
  const OmpReadyFrameSchema = z.object({
622
649
  type: z.literal("ready"),
@@ -1217,7 +1244,7 @@ function runtimeFrameCollectionLimit(frame: Record<string, unknown>): number {
1217
1244
  return 1_024;
1218
1245
  }
1219
1246
  const OmpModelsResultSchema = z.object({
1220
- models: z.array(OmpModelSchema).min(1).max(256),
1247
+ models: z.array(OmpModelSchema).min(1).max(MAX_MODEL_CATALOG_ITEMS),
1221
1248
  });
1222
1249
  const OmpPromptAckSchema = z.object({ agentInvoked: z.boolean().optional() }).optional();
1223
1250
  const OmpAvailableCommandsResultSchema = z.object({
@@ -1262,6 +1289,7 @@ const ProtocolNegotiationResultSchema = z.object({
1262
1289
  });
1263
1290
 
1264
1291
  export type OmpModel = z.infer<typeof OmpModelSchema>;
1292
+ export type OmpBranchResult = z.infer<typeof OmpBranchResultSchema>;
1265
1293
  export type OmpSessionState = z.infer<typeof OmpSessionStateSchema>;
1266
1294
  export type OmpSessionStats = z.infer<typeof OmpSessionStatsSchema>;
1267
1295
  export type OmpCompactionResult = z.infer<typeof OmpCompactionResultSchema>;
@@ -1369,7 +1397,7 @@ export interface OmpRuntimeSession {
1369
1397
  respondToExtensionUi(response: OmpExtensionUiResponse): Promise<void>;
1370
1398
  respondToToolApproval(response: OmpToolApprovalResponse): Promise<void>;
1371
1399
  getBranchMessages(): Promise<Array<{ entryId: string; text: string }>>;
1372
- branch(entryId: string): Promise<{ text: string; cancelled: boolean }>;
1400
+ branch(entryId: string): Promise<OmpBranchResult>;
1373
1401
  readonly canReplayHistory: boolean;
1374
1402
  getMessages(): Promise<OmpMessage[]>;
1375
1403
  abort(): Promise<void>;
@@ -1416,6 +1444,13 @@ export interface OmpRpcRuntimeOptions {
1416
1444
  ) => OmpSessionDescriptor[] | Promise<OmpSessionDescriptor[]>;
1417
1445
  }
1418
1446
 
1447
+ export class OmpRpcRequestRejectedError extends Error {
1448
+ constructor() {
1449
+ super("OMP RPC request failed");
1450
+ this.name = "OmpRpcRequestRejectedError";
1451
+ }
1452
+ }
1453
+
1419
1454
  type PendingRequest = {
1420
1455
  resolve(value: unknown): void;
1421
1456
  reject(error: Error): void;
@@ -2472,13 +2507,19 @@ class OmpRpcProcess {
2472
2507
  : response.data.data;
2473
2508
  const boundedFrame =
2474
2509
  responseData === response.data.data ? frame : { ...frame, data: responseData };
2475
- const responseItemLimit = isBranchHistory ? 1_024 : isHistory ? 100_000 : MAX_ARRAY_ITEMS;
2510
+ const responseItemLimit = isBranchHistory
2511
+ ? 1_024
2512
+ : isHistory
2513
+ ? 100_000
2514
+ : pending.command === "get_available_models"
2515
+ ? MAX_MODEL_CATALOG_ITEMS
2516
+ : MAX_ARRAY_ITEMS;
2476
2517
  const responseByteLimit =
2477
2518
  isBranchHistory || isHistory
2478
2519
  ? Math.min(MAX_REASSEMBLED_FRAME_BYTES, this.reassembledFrameLimit)
2479
2520
  : 2 * 1024 * 1024;
2480
- // A model catalog contains up to 256 structured models, so its aggregate
2481
- // node budget must exceed the small state/command-response budget.
2521
+ // Model catalogs are truncated before publication, while the transport still enforces
2522
+ // aggregate byte and node budgets over the complete response.
2482
2523
  const responseNodeLimit = isBranchHistory
2483
2524
  ? 4_096
2484
2525
  : isHistory
@@ -2517,7 +2558,7 @@ class OmpRpcProcess {
2517
2558
  settled.reject(new Error("OMP RPC response is invalid"));
2518
2559
  }
2519
2560
  } else {
2520
- settled.reject(new Error("OMP RPC request failed"));
2561
+ settled.reject(new OmpRpcRequestRejectedError());
2521
2562
  }
2522
2563
  }
2523
2564
 
@@ -11,7 +11,15 @@ 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 {
15
+ mapOmpModels,
16
+ nativeOmpModelId,
17
+ OMP_MODES,
18
+ ompModelId,
19
+ selectOmpModels,
20
+ thinkingForModel,
21
+ validateOmpModelIdentities,
22
+ } from "./catalog";
15
23
  import {
16
24
  normalizeOmpSessionConfig,
17
25
  type OmpRecoveryOptions,
@@ -21,6 +29,7 @@ import { OmpHostToolsBridge, type OmpMcpConnector, validateOmpHostToolConfig } f
21
29
  import { isOmpImageMimeType, isValidImagePayload, OmpImageMaterializer } from "./image";
22
30
  import type {
23
31
  OmpAvailableCommand,
32
+ OmpBranchResult,
24
33
  OmpCompactionResult,
25
34
  OmpExtensionUiResponse,
26
35
  OmpImage,
@@ -35,7 +44,7 @@ import type {
35
44
  OmpToolApprovalCancel,
36
45
  OmpToolApprovalRequest,
37
46
  } from "./omp-rpc";
38
- import { buildOmpSpawnRequest } from "./omp-rpc";
47
+ import { buildOmpSpawnRequest, OmpRpcRequestRejectedError } from "./omp-rpc";
39
48
  import {
40
49
  BoundedStringSet,
41
50
  boundedJsonBytes,
@@ -938,6 +947,8 @@ export class OmpProviderSession {
938
947
  private readonly pendingToolPermissions = new Map<string, PendingToolPermission>();
939
948
  private readonly inFlightToolPermissions = new Map<string, PendingToolPermission>();
940
949
  private readonly resolvedToolApprovalIds = new BoundedStringSet(MAX_TRACKED_ENTRY_IDS);
950
+ private unsupportedThinkingNoticePending = false;
951
+ private unsupportedThinkingNoticePublished = false;
941
952
 
942
953
  private constructor(
943
954
  id: string,
@@ -1056,10 +1067,9 @@ export class OmpProviderSession {
1056
1067
  }
1057
1068
  const startOptions: OmpStartOptions = {
1058
1069
  ...normalizedConfig,
1059
- // Model selection is authorized only after this runtime reports its exact catalog.
1060
- ...(resumeSessionId
1061
- ? { thinkingOption: undefined, systemPrompt: undefined, resumeSessionId }
1062
- : {}),
1070
+ // Thinking is authorized only after this runtime reports its exact model catalog.
1071
+ thinkingOption: undefined,
1072
+ ...(resumeSessionId ? { systemPrompt: undefined, resumeSessionId } : {}),
1063
1073
  signal,
1064
1074
  environment,
1065
1075
  };
@@ -1095,6 +1105,7 @@ export class OmpProviderSession {
1095
1105
  () => ({ available: false, commands: [] }),
1096
1106
  ),
1097
1107
  ]);
1108
+ validateOmpModelIdentities(nativeModels);
1098
1109
  if (effectiveConfig.persist && !native.canReplayHistory) {
1099
1110
  throw new OmpPublicError("OMP session persistence requires negotiated RPC protocol v2");
1100
1111
  }
@@ -1108,12 +1119,10 @@ export class OmpProviderSession {
1108
1119
  if (resumeSessionId && initialState.sessionId !== resumeSessionId) {
1109
1120
  throw new OmpPublicError("OMP resumed a different native session");
1110
1121
  }
1111
- const models = mapOmpModels(nativeModels, new OmpPublicDataSerializer(outputRedactionValues));
1112
- const nativeModelsByPublicId = new Map(
1113
- nativeModels.map((model) => [ompModelId(model), model] as const),
1114
- );
1115
1122
  if (!resumeSessionId && input.config.model) {
1116
- const selected = nativeModelsByPublicId.get(input.config.model);
1123
+ const selected = selectOmpModels(nativeModels, state.model).find(
1124
+ (model) => ompModelId(model) === input.config.model,
1125
+ );
1117
1126
  if (!selected) {
1118
1127
  throw new OmpPublicError("OMP model is not advertised by the configured session runtime");
1119
1128
  }
@@ -1122,9 +1131,29 @@ export class OmpProviderSession {
1122
1131
  state = await native.getState();
1123
1132
  }
1124
1133
  }
1134
+ let currentModel = state.model
1135
+ ? nativeModels.find(
1136
+ (model) => model.provider === state.model?.provider && model.id === state.model.id,
1137
+ )
1138
+ : undefined;
1139
+ if (state.model && !currentModel) {
1140
+ throw new OmpPublicError("OMP runtime selected an unadvertised model");
1141
+ }
1142
+ if (!resumeSessionId && input.config.thinkingOption !== undefined) {
1143
+ if (
1144
+ !thinkingForModel(currentModel).some(
1145
+ (option) => option.id === input.config.thinkingOption,
1146
+ )
1147
+ ) {
1148
+ throw new OmpPublicError("OMP thinking level is unavailable for the selected model");
1149
+ }
1150
+ if (state.thinkingLevel !== input.config.thinkingOption) {
1151
+ await native.setThinkingLevel(input.config.thinkingOption);
1152
+ }
1153
+ }
1125
1154
  const reconciledConfigRevision = bootstrapConfigRevision;
1126
1155
  state = await native.getState();
1127
- const currentModel = state.model
1156
+ currentModel = state.model
1128
1157
  ? nativeModels.find(
1129
1158
  (model) => model.provider === state.model?.provider && model.id === state.model.id,
1130
1159
  )
@@ -1132,21 +1161,21 @@ export class OmpProviderSession {
1132
1161
  if (state.model && !currentModel) {
1133
1162
  throw new OmpPublicError("OMP runtime selected an unadvertised model");
1134
1163
  }
1164
+ const selectedNativeModels = selectOmpModels(nativeModels, state.model);
1165
+ const models = mapOmpModels(
1166
+ selectedNativeModels,
1167
+ new OmpPublicDataSerializer(outputRedactionValues),
1168
+ );
1169
+ const nativeModelsByPublicId = new Map(
1170
+ nativeModels.map((model) => [ompModelId(model), model] as const),
1171
+ );
1135
1172
  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
- }
1173
+ const applicableLevel = applicableThinkingLevel(currentModel, state.thinkingLevel);
1174
+ const committedThinkingLevel = thinkingOptions.some((option) => option.id === applicableLevel)
1175
+ ? applicableLevel
1176
+ : undefined;
1177
+ const unsupportedThinkingLevel =
1178
+ applicableLevel !== undefined && committedThinkingLevel === undefined;
1150
1179
  const configState: ProviderConfigState = {
1151
1180
  ...(state.model ? { model: ompModelId(state.model) } : {}),
1152
1181
  mode: normalizedConfig.mode ?? "full",
@@ -1209,6 +1238,7 @@ export class OmpProviderSession {
1209
1238
  retireRewindSession,
1210
1239
  scheduler,
1211
1240
  );
1241
+ session.unsupportedThinkingNoticePending = unsupportedThinkingLevel;
1212
1242
 
1213
1243
  if (bootstrapConfigRevision !== reconciledConfigRevision) {
1214
1244
  session.configRefreshDirty = true;
@@ -1267,6 +1297,7 @@ export class OmpProviderSession {
1267
1297
  : {}),
1268
1298
  ...(this.config.title ? { title: this.dataFilter.text(this.config.title, 256) } : {}),
1269
1299
  });
1300
+ this.publishUnsupportedThinkingNotice();
1270
1301
  this.emit({ type: "session.config", sessionId: this.id, config: this.configState });
1271
1302
  if (this.replayHistoryOnOpen) await this.replayHistory(true);
1272
1303
  this.publishCommands(this.commandCatalog);
@@ -1274,6 +1305,22 @@ export class OmpProviderSession {
1274
1305
  this.readyPublished = true;
1275
1306
  if (this.configRefreshDirty) this.scheduleCommittedConfigRefresh();
1276
1307
  }
1308
+ private publishUnsupportedThinkingNotice(): void {
1309
+ if (!this.unsupportedThinkingNoticePending || this.unsupportedThinkingNoticePublished) return;
1310
+ this.unsupportedThinkingNoticePending = false;
1311
+ this.unsupportedThinkingNoticePublished = true;
1312
+ this.emit({
1313
+ type: "session.notice",
1314
+ sessionId: this.id,
1315
+ notice: {
1316
+ id: "omp:unsupported-thinking-level",
1317
+ severity: "warning",
1318
+ title: "OMP thinking level unavailable",
1319
+ description:
1320
+ "OMP reported a thinking level that the active model does not advertise. Paseo omitted it from the session configuration.",
1321
+ },
1322
+ });
1323
+ }
1277
1324
 
1278
1325
  private usageFrom(
1279
1326
  state: OmpSessionState | undefined,
@@ -1679,19 +1726,33 @@ export class OmpProviderSession {
1679
1726
  throw new OmpPublicError("OMP conversation rewind requires negotiated RPC protocol v2");
1680
1727
  }
1681
1728
  const entryId = this.projector.resolveRevertToken(input.token);
1682
- const [branchMessages, beforeState] = await Promise.all([
1683
- runtime.getBranchMessages(),
1684
- runtime.getState(),
1685
- ]);
1729
+ const beforeState = await runtime.getState();
1686
1730
  this.requireCurrentRuntime(runtime, generation);
1687
1731
  if (this.activeTurn || beforeState.isStreaming || beforeState.isCompacting) {
1688
1732
  throw new OmpPublicError("Cannot rewind the OMP conversation while a turn is active");
1689
1733
  }
1690
- if (!branchMessages.some((message) => message.entryId === entryId)) {
1691
- throw new OmpPublicError("OMP conversation rewind token is stale");
1734
+ const beforeAdvertisedModel = beforeState.model
1735
+ ? this.nativeModelsByPublicId.get(ompModelId(beforeState.model))
1736
+ : undefined;
1737
+ if (beforeState.model && !beforeAdvertisedModel) {
1738
+ throw new OmpPublicError("OMP runtime selected an unadvertised model");
1692
1739
  }
1740
+ const restorableThinkingLevel = thinkingForModel(beforeAdvertisedModel).some(
1741
+ (option) => option.id === beforeState.thinkingLevel,
1742
+ )
1743
+ ? beforeState.thinkingLevel
1744
+ : undefined;
1693
1745
  branchMutationPossible = true;
1694
- const result = await runtime.branch(entryId);
1746
+ let result: OmpBranchResult;
1747
+ try {
1748
+ result = await runtime.branch(entryId);
1749
+ } catch (error) {
1750
+ if (error instanceof OmpRpcRequestRejectedError) {
1751
+ branchMutationPossible = false;
1752
+ throw new OmpPublicError("OMP conversation rewind token is stale");
1753
+ }
1754
+ throw error;
1755
+ }
1695
1756
  this.requireCurrentRuntime(runtime, generation);
1696
1757
  if (result.cancelled) {
1697
1758
  branchMutationPossible = false;
@@ -1723,13 +1784,10 @@ export class OmpProviderSession {
1723
1784
  }
1724
1785
  await runtime.setModel(beforeState.model.provider, beforeState.model.id);
1725
1786
  }
1726
- if (thinkingChanged) {
1727
- if (!beforeState.thinkingLevel) {
1728
- throw new OmpPublicError("OMP changed thinking level while rewinding the conversation");
1729
- }
1730
- await runtime.setThinkingLevel(beforeState.thinkingLevel);
1787
+ if (thinkingChanged && restorableThinkingLevel) {
1788
+ await runtime.setThinkingLevel(restorableThinkingLevel);
1731
1789
  }
1732
- if (modelChanged || thinkingChanged) {
1790
+ if (modelChanged || (thinkingChanged && restorableThinkingLevel)) {
1733
1791
  state = await runtime.getState();
1734
1792
  this.requireCurrentRuntime(runtime, generation);
1735
1793
  }
@@ -1737,7 +1795,7 @@ export class OmpProviderSession {
1737
1795
  state.sessionId !== this.nativeSessionId ||
1738
1796
  state.model?.provider !== beforeState.model?.provider ||
1739
1797
  state.model?.id !== beforeState.model?.id ||
1740
- state.thinkingLevel !== beforeState.thinkingLevel
1798
+ (restorableThinkingLevel !== undefined && state.thinkingLevel !== restorableThinkingLevel)
1741
1799
  ) {
1742
1800
  throw new OmpPublicError("OMP did not preserve session configuration while rewinding");
1743
1801
  }
@@ -2337,7 +2395,10 @@ export class OmpProviderSession {
2337
2395
  const targetModel = targetModelId
2338
2396
  ? this.nativeModelsByPublicId.get(targetModelId)
2339
2397
  : undefined;
2340
- if (input.changes.model !== undefined && !targetModel) {
2398
+ const targetModelPublished = this.configState.models.some(
2399
+ (model) => model.id === targetModelId,
2400
+ );
2401
+ if (input.changes.model !== undefined && (!targetModel || !targetModelPublished)) {
2341
2402
  throw new OmpPublicError("OMP model selection is unavailable");
2342
2403
  }
2343
2404
  if (
@@ -2584,13 +2645,18 @@ export class OmpProviderSession {
2584
2645
  throw new OmpCatalogEscape("OMP runtime selected an unadvertised model");
2585
2646
  }
2586
2647
  const thinkingOptions = thinkingForModel(advertisedModel);
2587
- const committedThinkingLevel = applicableThinkingLevel(advertisedModel, state.thinkingLevel);
2588
- if (
2589
- committedThinkingLevel &&
2590
- !thinkingOptions.some((option) => option.id === committedThinkingLevel)
2591
- ) {
2592
- throw new OmpCatalogEscape("OMP runtime selected an unsupported thinking level");
2648
+ const applicableLevel = applicableThinkingLevel(advertisedModel, state.thinkingLevel);
2649
+ const committedThinkingLevel = thinkingOptions.some((option) => option.id === applicableLevel)
2650
+ ? applicableLevel
2651
+ : undefined;
2652
+ if (applicableLevel !== undefined && committedThinkingLevel === undefined) {
2653
+ this.unsupportedThinkingNoticePending = true;
2654
+ this.publishUnsupportedThinkingNotice();
2593
2655
  }
2656
+ const models = mapOmpModels(
2657
+ selectOmpModels([...this.nativeModelsByPublicId.values()], state.model),
2658
+ this.dataFilter,
2659
+ );
2594
2660
  this.configRefreshAttempts = 0;
2595
2661
  this.cancelConfigRefreshRetry();
2596
2662
  const nextConfig: ProviderConfigState = {
@@ -2599,12 +2665,15 @@ export class OmpProviderSession {
2599
2665
  ...(committedThinkingLevel
2600
2666
  ? { thinkingOption: committedThinkingLevel }
2601
2667
  : { thinkingOption: undefined }),
2668
+ models,
2602
2669
  thinkingOptions,
2603
2670
  };
2604
2671
  const changed =
2605
2672
  force ||
2606
2673
  nextConfig.model !== this.configState.model ||
2607
2674
  nextConfig.thinkingOption !== this.configState.thinkingOption ||
2675
+ nextConfig.models.some((model, index) => model.id !== this.configState.models[index]?.id) ||
2676
+ this.configState.models.length !== nextConfig.models.length ||
2608
2677
  nextConfig.thinkingOptions.length !== this.configState.thinkingOptions.length ||
2609
2678
  nextConfig.thinkingOptions.some(
2610
2679
  (option, index) =>
@@ -2925,13 +2994,6 @@ export class OmpProviderSession {
2925
2994
  if (state.model && !advertisedModel) {
2926
2995
  throw new Error("OMP recovered with an unadvertised model");
2927
2996
  }
2928
- const recoveredThinkingLevel = applicableThinkingLevel(advertisedModel, state.thinkingLevel);
2929
- if (
2930
- recoveredThinkingLevel &&
2931
- !thinkingForModel(advertisedModel).some((option) => option.id === recoveredThinkingLevel)
2932
- ) {
2933
- throw new Error("OMP recovered with an unsupported thinking level");
2934
- }
2935
2997
  if (this.closed) throw new Error("OMP session closed while runtime recovery was pending");
2936
2998
  if (!this.hostTools.isBoundTo(recovered)) {
2937
2999
  throw new Error("OMP host tool bridge detached during recovery");