@omercnet/paseo-omp 0.2.1 → 0.3.0-next.101.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 (64) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +25 -13
  3. package/SUPPORT.md +7 -3
  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 +248 -17
  41. package/server/provider/host-tools.ts +294 -26
  42. package/server/provider/omp-rpc.ts +499 -72
  43. package/server/provider/profile-providers.ts +249 -0
  44. package/server/provider/registration.ts +11 -0
  45. package/server/provider/security.ts +8 -10
  46. package/server/provider/session-descriptors.ts +340 -1
  47. package/server/provider/session.ts +716 -249
  48. package/server/provider/subsessions.ts +25 -2
  49. package/server/provider/timeline-projector.ts +104 -44
  50. package/server/provider-diagnostics.ts +122 -36
  51. package/server/quota.ts +3 -2
  52. package/server/sessions.ts +2 -2
  53. package/shared/composer-pill-settings.ts +28 -0
  54. package/shared/external-url.ts +21 -0
  55. package/shared/hub.ts +3 -3
  56. package/shared/mcp.ts +47 -0
  57. package/shared/memory.ts +2 -1
  58. package/shared/omp-config.ts +5 -1
  59. package/shared/omp-plugins.ts +74 -33
  60. package/shared/omp-settings.ts +8 -1
  61. package/shared/omp-store.ts +58 -0
  62. package/shared/provider-diagnostics.ts +12 -3
  63. package/shared/quota.ts +2 -1
  64. package/shared/sessions.ts +2 -1
@@ -2,12 +2,20 @@ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { isAbsolute, join } from "node:path";
4
4
  import { z } from "zod";
5
+ import { ompDataDir } from "../paths";
5
6
  import { isValidImagePayload } from "./image";
6
- import { boundedJsonBytes, OmpCleanupFailure, OmpPublicError, utf8Bytes } from "./security";
7
+ import {
8
+ boundedJsonBytes,
9
+ boundedJsonMetrics,
10
+ OmpCleanupFailure,
11
+ OmpPublicError,
12
+ utf8Bytes,
13
+ } from "./security";
7
14
  import {
8
15
  listOmpSessionDescriptors,
9
16
  type OmpSessionDescriptor,
10
17
  type OmpSessionListOptions,
18
+ readOmpPersistedSessionTranscript,
11
19
  readOmpPersistedSubagentTranscript,
12
20
  validateNativeSessionId,
13
21
  } from "./session-descriptors";
@@ -54,7 +62,17 @@ const MAX_PENDING_ONE_WAY_WRITES = 256;
54
62
  const MAX_PENDING_WRITE_BYTES = 8 * 1024 * 1024;
55
63
  const MAX_LINE_PARTS = 4_096;
56
64
  const MAX_ARRAY_ITEMS = 512;
57
- const MAX_CONTENT_PARTS = 64;
65
+ // OMP read metadata can contain one source entry per displayed line. Bound this optional,
66
+ // opaque field separately so over-budget metadata can be omitted without losing completion events.
67
+ const MAX_OPTIONAL_METADATA_BYTES = MAX_TOOL_PAYLOAD_LENGTH;
68
+ const MAX_OPTIONAL_METADATA_ITEMS = 2_048;
69
+ const MAX_OPTIONAL_METADATA_NODES = 4_096;
70
+ const MAX_TASK_CORRELATION_BYTES = 256 * 1024;
71
+ const MAX_TASK_CORRELATION_ITEMS = 1_024;
72
+ const MAX_TASK_CORRELATION_NODES = 4_096;
73
+ // Tool-intensive OMP turns legitimately exceed 64 blocks; transport byte/node budgets remain the
74
+ // primary resource bounds.
75
+ export const OMP_MAX_CONTENT_PARTS = 4_096;
58
76
  const MAX_TODOS = 256;
59
77
  const MAX_ENV_ENTRIES = 256;
60
78
  const MAX_ENV_VALUE_LENGTH = 64 * 1024;
@@ -63,6 +81,9 @@ const MAX_PATH_LENGTH = 4_096;
63
81
  const WINDOWS_DEFAULT_SYSTEM_ROOT = "C:\\Windows";
64
82
  const MAX_TOKEN_COUNT = Number.MAX_SAFE_INTEGER;
65
83
  const MAX_COST_USD = 1_000_000_000;
84
+ const MAX_RPC_ERROR_BYTES = 4_096;
85
+ const MAX_RPC_ERROR_CODE_BYTES = 256;
86
+ const PROMPT_SCHEDULING_FAILURE = "OMP prompt scheduling failed";
66
87
  const MAX_CONTEXT_PERCENT = 1_000_000;
67
88
  function boundedJsonString(maxBytes: number, minBytes = 0) {
68
89
  return z.string().refine((value) => {
@@ -99,6 +120,76 @@ const IDENTIFIER = boundedString(MAX_ID_LENGTH, 1);
99
120
  const NAME = boundedString(MAX_NAME_LENGTH, 1);
100
121
  const OMP_PROVIDER_NAME = NAME.refine((provider) => !provider.includes("/"));
101
122
  const TEXT = boundedString(MAX_TEXT_LENGTH);
123
+ const RAW_DISPLAY_TEXT = boundedString(MAX_IMAGE_DATA_LENGTH);
124
+ const DISPLAY_TRUNCATION_MARKER = "<truncated>";
125
+
126
+ function boundRawDisplayContent(value: unknown): unknown {
127
+ if (typeof value === "string") {
128
+ return utf8Bytes(value) <= MAX_IMAGE_DATA_LENGTH ? value : DISPLAY_TRUNCATION_MARKER;
129
+ }
130
+ if (!Array.isArray(value)) return value;
131
+ let totalBytes = 0;
132
+ for (const part of value) {
133
+ if (!part || typeof part !== "object" || Array.isArray(part)) continue;
134
+ const record = part as Record<string, unknown>;
135
+ if (typeof record.text === "string") totalBytes += utf8Bytes(record.text);
136
+ if (typeof record.thinking === "string") totalBytes += utf8Bytes(record.thinking);
137
+ }
138
+ if (totalBytes <= MAX_IMAGE_DATA_LENGTH) return value;
139
+
140
+ let retainedMarker = false;
141
+ return value.map((part) => {
142
+ if (!part || typeof part !== "object" || Array.isArray(part)) return part;
143
+ const copy = { ...(part as Record<string, unknown>) };
144
+ for (const key of ["text", "thinking"] as const) {
145
+ if (typeof copy[key] !== "string") continue;
146
+ if (retainedMarker) delete copy[key];
147
+ else {
148
+ copy[key] = DISPLAY_TRUNCATION_MARKER;
149
+ retainedMarker = true;
150
+ }
151
+ }
152
+ return copy;
153
+ });
154
+ }
155
+
156
+ function sanitizeLiveMessageDisplay(value: unknown): unknown {
157
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
158
+ const message = value as Record<string, unknown>;
159
+ if (message.role === "assistant") {
160
+ const content = boundRawDisplayContent(message.content);
161
+ return content === message.content ? value : { ...message, content };
162
+ }
163
+ if (
164
+ message.role === "bashExecution" &&
165
+ typeof message.output === "string" &&
166
+ utf8Bytes(message.output) > MAX_IMAGE_DATA_LENGTH
167
+ ) {
168
+ return { ...message, output: DISPLAY_TRUNCATION_MARKER };
169
+ }
170
+ return value;
171
+ }
172
+
173
+ function sanitizeLiveDisplayFrame(value: unknown): unknown {
174
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
175
+ const frame = value as Record<string, unknown>;
176
+ if (
177
+ frame.type === "message_start" ||
178
+ frame.type === "message_update" ||
179
+ frame.type === "message_end"
180
+ ) {
181
+ const message = sanitizeLiveMessageDisplay(frame.message);
182
+ return message === frame.message ? value : { ...frame, message };
183
+ }
184
+ if (frame.type !== "agent_end" || !Array.isArray(frame.messages)) return value;
185
+ let changed = false;
186
+ const messages = frame.messages.map((message) => {
187
+ const sanitized = sanitizeLiveMessageDisplay(message);
188
+ if (sanitized !== message) changed = true;
189
+ return sanitized;
190
+ });
191
+ return changed ? { ...frame, messages } : value;
192
+ }
102
193
  const OmpThinkingLevelSchema = z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
103
194
 
104
195
  function isBoundedJson(
@@ -111,6 +202,148 @@ function isBoundedJson(
111
202
  boundedJsonBytes(value, maxBytes, maxItems, maxBytes, maxNodes) !== Number.POSITIVE_INFINITY
112
203
  );
113
204
  }
205
+ function optionalMetadataMetrics(value: unknown) {
206
+ return boundedJsonMetrics(
207
+ value,
208
+ MAX_OPTIONAL_METADATA_BYTES,
209
+ MAX_OPTIONAL_METADATA_ITEMS,
210
+ MAX_OPTIONAL_METADATA_BYTES,
211
+ MAX_OPTIONAL_METADATA_NODES,
212
+ );
213
+ }
214
+
215
+ function optionalMetadataIsBounded(value: unknown): boolean {
216
+ return optionalMetadataMetrics(value) !== undefined;
217
+ }
218
+
219
+ function omitUnsafeOptionalDetails(value: unknown): unknown {
220
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
221
+ const record = value as Record<string, unknown>;
222
+ if (!Object.hasOwn(record, "details") || optionalMetadataIsBounded(record.details)) return value;
223
+ const { details: _details, ...safe } = record;
224
+ return safe;
225
+ }
226
+
227
+ const TASK_RESULT_STATUSES: Readonly<Record<string, true>> = {
228
+ pending: true,
229
+ running: true,
230
+ completed: true,
231
+ failed: true,
232
+ error: true,
233
+ aborted: true,
234
+ canceled: true,
235
+ cancelled: true,
236
+ };
237
+ const TASK_PROGRESS_STATUSES: Readonly<Record<string, true>> = {
238
+ pending: true,
239
+ running: true,
240
+ completed: true,
241
+ failed: true,
242
+ aborted: true,
243
+ };
244
+
245
+ function boundedTaskId(value: unknown): value is string {
246
+ return typeof value === "string" && value.length > 0 && utf8Bytes(value) <= MAX_ID_LENGTH;
247
+ }
248
+
249
+ function taskCorrelationDetails(value: unknown): unknown {
250
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return;
251
+ const details = value as Record<string, unknown>;
252
+ if (!Array.isArray(details.results) || details.results.length > MAX_TASK_CORRELATION_ITEMS)
253
+ return;
254
+ const results: Record<string, unknown>[] = [];
255
+ for (const value of details.results) {
256
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return;
257
+ const result = value as Record<string, unknown>;
258
+ if (!boundedTaskId(result.id)) return;
259
+ const safe: Record<string, unknown> = { id: result.id };
260
+ if (typeof result.status === "string" && Object.hasOwn(TASK_RESULT_STATUSES, result.status)) {
261
+ safe.status = result.status;
262
+ }
263
+ if (typeof result.aborted === "boolean") safe.aborted = result.aborted;
264
+ if (typeof result.exitCode === "number" && Number.isFinite(result.exitCode)) {
265
+ safe.exitCode = result.exitCode;
266
+ }
267
+ if (
268
+ result.error !== undefined &&
269
+ boundedJsonMetrics(result.error, 4_096, 32, 4_096, 64) !== undefined
270
+ ) {
271
+ safe.error = result.error;
272
+ }
273
+ results.push(safe);
274
+ }
275
+ let progress: Record<string, unknown>[] | undefined;
276
+ if (details.progress !== undefined) {
277
+ if (!Array.isArray(details.progress) || details.progress.length > MAX_TASK_CORRELATION_ITEMS) {
278
+ return;
279
+ }
280
+ progress = [];
281
+ for (const value of details.progress) {
282
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return;
283
+ const item = value as Record<string, unknown>;
284
+ if (
285
+ !boundedTaskId(item.id) ||
286
+ typeof item.index !== "number" ||
287
+ !Number.isInteger(item.index) ||
288
+ item.index < 0 ||
289
+ item.index >= MAX_TASK_CORRELATION_ITEMS ||
290
+ typeof item.status !== "string" ||
291
+ !Object.hasOwn(TASK_PROGRESS_STATUSES, item.status)
292
+ ) {
293
+ return;
294
+ }
295
+ progress.push({ id: item.id, index: item.index, status: item.status });
296
+ }
297
+ }
298
+ const correlation = { results, ...(progress ? { progress } : {}) };
299
+ return boundedJsonMetrics(
300
+ correlation,
301
+ MAX_TASK_CORRELATION_BYTES,
302
+ MAX_TASK_CORRELATION_ITEMS,
303
+ MAX_TASK_CORRELATION_BYTES,
304
+ MAX_TASK_CORRELATION_NODES,
305
+ )
306
+ ? correlation
307
+ : undefined;
308
+ }
309
+
310
+ function omitOptionalDetails(value: unknown, preserveTaskCorrelation = false): unknown {
311
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
312
+ const record = value as Record<string, unknown>;
313
+ if (!Object.hasOwn(record, "details")) return value;
314
+ const { details: _details, ...structural } = record;
315
+ if (!preserveTaskCorrelation) return structural;
316
+ const correlation = taskCorrelationDetails(record.details);
317
+ return correlation === undefined ? structural : { ...structural, details: correlation };
318
+ }
319
+
320
+ function createOptionalMetadataSanitizer(): (value: unknown, taskResult?: boolean) => unknown {
321
+ let retainedBytes = 0;
322
+ let retainedNodes = 0;
323
+ return (value, taskResult = false) => {
324
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
325
+ const record = value as Record<string, unknown>;
326
+ if (!Object.hasOwn(record, "details")) return value;
327
+ const metrics = optionalMetadataMetrics(record.details);
328
+ if (
329
+ metrics &&
330
+ retainedBytes + metrics.bytes <= MAX_OPTIONAL_METADATA_BYTES &&
331
+ retainedNodes + metrics.nodes <= MAX_OPTIONAL_METADATA_NODES
332
+ ) {
333
+ retainedBytes += metrics.bytes;
334
+ retainedNodes += metrics.nodes;
335
+ return value;
336
+ }
337
+ const correlation = taskResult ? taskCorrelationDetails(record.details) : undefined;
338
+ const { details: _details, ...safe } = record;
339
+ return correlation === undefined ? safe : { ...safe, details: correlation };
340
+ };
341
+ }
342
+
343
+ const OmpOptionalMetadataSchema = z.preprocess(
344
+ (value) => (optionalMetadataIsBounded(value) ? value : undefined),
345
+ z.unknown().optional(),
346
+ );
114
347
 
115
348
  const OmpContentPartSchema = z
116
349
  .object({
@@ -140,13 +373,21 @@ const OmpContentPartSchema = z
140
373
  context.addIssue({ code: "custom", message: "invalid image payload" });
141
374
  }
142
375
  });
376
+ const OmpAssistantContentPartSchema = OmpContentPartSchema.safeExtend({
377
+ text: RAW_DISPLAY_TEXT.optional(),
378
+ thinking: RAW_DISPLAY_TEXT.optional(),
379
+ });
380
+ const OmpAssistantDisplayContentSchema = z.preprocess(
381
+ boundRawDisplayContent,
382
+ z.union([RAW_DISPLAY_TEXT, z.array(OmpAssistantContentPartSchema).max(OMP_MAX_CONTENT_PARTS)]),
383
+ );
143
384
  const OmpDisplayContentSchema = z.union([
144
385
  TEXT,
145
- z.array(OmpContentPartSchema).max(MAX_CONTENT_PARTS),
386
+ z.array(OmpContentPartSchema).max(OMP_MAX_CONTENT_PARTS),
146
387
  ]);
147
388
  const OmpImageArraySchema = z
148
389
  .array(OmpContentPartSchema)
149
- .max(MAX_CONTENT_PARTS)
390
+ .max(OMP_MAX_CONTENT_PARTS)
150
391
  .superRefine((parts, context) => {
151
392
  if (parts.some((part) => part.type !== "image")) {
152
393
  context.addIssue({ code: "custom", message: "invalid image collection" });
@@ -158,10 +399,7 @@ const OmpMessageIdentityShape = {
158
399
  responseId: IDENTIFIER.optional(),
159
400
  images: OmpImageArraySchema.optional(),
160
401
  timestamp: z.number().finite().optional(),
161
- details: z
162
- .unknown()
163
- .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096))
164
- .optional(),
402
+ details: OmpOptionalMetadataSchema,
165
403
  };
166
404
  type OmpContentPart = z.infer<typeof OmpContentPartSchema>;
167
405
  type OmpMessageIdentity = {
@@ -211,7 +449,7 @@ export type OmpMessage = OmpMessageIdentity &
211
449
  const OmpMessageSchema: z.ZodType<OmpMessage> = z.union([
212
450
  z.object({
213
451
  role: z.literal("assistant"),
214
- content: OmpDisplayContentSchema.optional(),
452
+ content: OmpAssistantDisplayContentSchema.optional(),
215
453
  ...OmpMessageIdentityShape,
216
454
  errorMessage: boundedString(4_096).nullable().optional(),
217
455
  stopReason: boundedString(64).optional(),
@@ -234,7 +472,13 @@ const OmpMessageSchema: z.ZodType<OmpMessage> = z.union([
234
472
  z.object({
235
473
  role: z.literal("bashExecution"),
236
474
  command: TEXT,
237
- output: TEXT.optional(),
475
+ output: z.preprocess(
476
+ (value) =>
477
+ typeof value === "string" && utf8Bytes(value) > MAX_IMAGE_DATA_LENGTH
478
+ ? DISPLAY_TRUNCATION_MARKER
479
+ : value,
480
+ RAW_DISPLAY_TEXT.optional(),
481
+ ),
238
482
  exitCode: z.number().int().nullable().optional(),
239
483
  cancelled: z.boolean().optional(),
240
484
  truncated: z.boolean().optional(),
@@ -259,7 +503,7 @@ const OmpAssistantMessageEventSchema = z
259
503
  .number()
260
504
  .int()
261
505
  .nonnegative()
262
- .max(MAX_CONTENT_PARTS - 1)
506
+ .max(OMP_MAX_CONTENT_PARTS - 1)
263
507
  .optional(),
264
508
  delta: TEXT.optional(),
265
509
  content: z
@@ -387,7 +631,8 @@ const OmpResponseFrameSchema = z.object({
387
631
  id: IDENTIFIER,
388
632
  success: z.boolean(),
389
633
  data: z.unknown().optional(),
390
- error: boundedString(4_096).optional(),
634
+ error: boundedString(MAX_RPC_ERROR_BYTES).optional(),
635
+ code: boundedString(MAX_RPC_ERROR_CODE_BYTES, 1).optional(),
391
636
  });
392
637
  const OmpChunkFrameSchema = z.object({
393
638
  type: z.literal("rpc_chunk"),
@@ -401,6 +646,16 @@ const JsonObjectSchema = z.record(z.string(), z.unknown());
401
646
  const BoundedToolPayloadSchema = z
402
647
  .unknown()
403
648
  .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096));
649
+ const OmpToolResultPayloadSchema = z.preprocess(
650
+ omitUnsafeOptionalDetails,
651
+ z
652
+ .unknown()
653
+ .refine(
654
+ (value) =>
655
+ isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, MAX_OPTIONAL_METADATA_ITEMS, 8_192) &&
656
+ isBoundedJson(omitOptionalDetails(value), MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096),
657
+ ),
658
+ );
404
659
  const OmpHostToolDefinitionSchema = z.object({
405
660
  name: NAME,
406
661
  label: NAME.optional(),
@@ -562,6 +817,7 @@ const OmpToolApprovalResponseSchema = z.union([
562
817
  ]);
563
818
  const OmpAgentEndEnvelopeSchema = z.object({
564
819
  type: z.literal("agent_end"),
820
+ requestId: IDENTIFIER.optional(),
565
821
  messageCount: z.number().int().nonnegative().optional(),
566
822
  isTerminal: z.boolean().optional(),
567
823
  });
@@ -582,6 +838,7 @@ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
582
838
  z.object({ type: z.literal("agent_start") }),
583
839
  z.object({
584
840
  type: z.literal("agent_end"),
841
+ requestId: IDENTIFIER.optional(),
585
842
  messages: z.array(OmpMessageSchema).max(MAX_ARRAY_ITEMS).optional(),
586
843
  messageCount: z.number().int().nonnegative().optional(),
587
844
  isTerminal: z.boolean().optional(),
@@ -606,13 +863,13 @@ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
606
863
  toolCallId: IDENTIFIER,
607
864
  toolName: NAME,
608
865
  args: BoundedToolPayloadSchema.optional(),
609
- partialResult: BoundedToolPayloadSchema,
866
+ partialResult: OmpToolResultPayloadSchema,
610
867
  }),
611
868
  z.object({
612
869
  type: z.literal("tool_execution_end"),
613
870
  toolCallId: IDENTIFIER,
614
871
  toolName: NAME,
615
- result: BoundedToolPayloadSchema,
872
+ result: OmpToolResultPayloadSchema,
616
873
  isError: z.boolean().optional(),
617
874
  }),
618
875
  OmpCompactionStartSchema,
@@ -863,6 +1120,102 @@ const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
863
1120
  OmpToolApprovalCancelSchema,
864
1121
  z.object({ type: z.literal("advisor_yielded") }),
865
1122
  ]);
1123
+ type OptionalDetailsMapper = (value: unknown, taskResult?: boolean) => unknown;
1124
+
1125
+ function mapRecordField(
1126
+ record: Record<string, unknown>,
1127
+ key: string,
1128
+ map: OptionalDetailsMapper,
1129
+ taskResult = false,
1130
+ ): Record<string, unknown> {
1131
+ if (!Object.hasOwn(record, key)) return record;
1132
+ const next = map(record[key], taskResult);
1133
+ return next === record[key] ? record : { ...record, [key]: next };
1134
+ }
1135
+
1136
+ function mapMessageList(value: unknown, map: OptionalDetailsMapper): unknown {
1137
+ if (!Array.isArray(value)) return value;
1138
+ let changed = false;
1139
+ const messages = value.map((message) => {
1140
+ const taskResult =
1141
+ message !== null &&
1142
+ typeof message === "object" &&
1143
+ !Array.isArray(message) &&
1144
+ (message as Record<string, unknown>).role === "toolResult" &&
1145
+ (message as Record<string, unknown>).toolName === "task";
1146
+ const next = map(message, taskResult);
1147
+ changed ||= next !== message;
1148
+ return next;
1149
+ });
1150
+ return changed ? messages : value;
1151
+ }
1152
+
1153
+ function mapAgentEventDetails(
1154
+ frame: Record<string, unknown>,
1155
+ map: OptionalDetailsMapper,
1156
+ ): Record<string, unknown> {
1157
+ switch (frame.type) {
1158
+ case "message_start":
1159
+ case "message_update":
1160
+ case "message_end": {
1161
+ const message = frame.message;
1162
+ const taskResult =
1163
+ message !== null &&
1164
+ typeof message === "object" &&
1165
+ !Array.isArray(message) &&
1166
+ (message as Record<string, unknown>).role === "toolResult" &&
1167
+ (message as Record<string, unknown>).toolName === "task";
1168
+ return mapRecordField(frame, "message", map, taskResult);
1169
+ }
1170
+ case "tool_execution_update":
1171
+ return mapRecordField(frame, "partialResult", map, frame.toolName === "task");
1172
+ case "tool_execution_end":
1173
+ return mapRecordField(frame, "result", map, frame.toolName === "task");
1174
+ case "agent_end":
1175
+ return mapRecordField(frame, "messages", (messages) => mapMessageList(messages, map));
1176
+ default:
1177
+ return frame;
1178
+ }
1179
+ }
1180
+
1181
+ function mapRuntimeFrameDetails(
1182
+ frame: Record<string, unknown>,
1183
+ map: OptionalDetailsMapper,
1184
+ ): Record<string, unknown> {
1185
+ if (frame.type !== "subagent_event") return mapAgentEventDetails(frame, map);
1186
+ if (frame.payload === null || typeof frame.payload !== "object" || Array.isArray(frame.payload)) {
1187
+ return frame;
1188
+ }
1189
+ const payload = frame.payload as Record<string, unknown>;
1190
+ if (payload.event === null || typeof payload.event !== "object" || Array.isArray(payload.event)) {
1191
+ return frame;
1192
+ }
1193
+ const event = mapAgentEventDetails(payload.event as Record<string, unknown>, map);
1194
+ return event === payload.event ? frame : { ...frame, payload: { ...payload, event } };
1195
+ }
1196
+
1197
+ function sanitizeMessageListMetadata(value: unknown): unknown {
1198
+ return mapMessageList(value, createOptionalMetadataSanitizer());
1199
+ }
1200
+
1201
+ function sanitizeHistoryResponseData(value: unknown): unknown {
1202
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
1203
+ return mapRecordField(value as Record<string, unknown>, "messages", sanitizeMessageListMetadata);
1204
+ }
1205
+
1206
+ function runtimeFrameCollectionLimit(frame: Record<string, unknown>): number {
1207
+ const type = frame.type;
1208
+ if (
1209
+ type === "message_start" ||
1210
+ type === "message_update" ||
1211
+ type === "message_end" ||
1212
+ type === "agent_end" ||
1213
+ type === "subagent_event"
1214
+ ) {
1215
+ return OMP_MAX_CONTENT_PARTS;
1216
+ }
1217
+ return 1_024;
1218
+ }
866
1219
  const OmpModelsResultSchema = z.object({
867
1220
  models: z.array(OmpModelSchema).min(1).max(256),
868
1221
  });
@@ -924,7 +1277,7 @@ export function parseOmpHostToolAgentResult(value: unknown): OmpHostToolResult["
924
1277
  }
925
1278
  export type OmpRpcEvent =
926
1279
  | z.infer<typeof OmpRuntimeEventSchema>
927
- | { type: "prompt_error"; id: string; error: string }
1280
+ | { type: "prompt_error"; id: string; error: string; code?: string }
928
1281
  | { type: "process_exit"; error: string };
929
1282
  export type OmpAgentSessionEvent = z.infer<typeof OmpAgentSessionEventSchema>;
930
1283
  export type OmpSubagentSnapshot = z.infer<typeof OmpSubagentsResultSchema>["subagents"][number];
@@ -945,6 +1298,13 @@ export interface OmpPersistedSubagentMessages {
945
1298
  byteLength: number;
946
1299
  messages: OmpMessage[];
947
1300
  }
1301
+ export interface OmpPersistedSessionMessages {
1302
+ sessionFile: string;
1303
+ nativeSessionId: string;
1304
+ byteLength: number;
1305
+ messages: OmpMessage[];
1306
+ imageReplayWarning?: true;
1307
+ }
948
1308
 
949
1309
  export interface OmpStartOptions {
950
1310
  cwd: string;
@@ -997,6 +1357,7 @@ export interface OmpRuntimeSession {
997
1357
  message: string,
998
1358
  images?: readonly OmpImage[],
999
1359
  onAccepted?: () => void,
1360
+ onRequested?: (requestId: string) => void,
1000
1361
  ): Promise<{ requestId: string; agentInvoked?: boolean }>;
1001
1362
  compact(customInstructions?: string): Promise<OmpCompactionResult>;
1002
1363
  setAutoCompaction(enabled: boolean): Promise<void>;
@@ -1022,6 +1383,12 @@ export interface OmpRuntime {
1022
1383
  readonly supportsPersistence: boolean;
1023
1384
  startSession(options: OmpStartOptions): Promise<OmpRuntimeSession>;
1024
1385
  listSessions(options: OmpSessionListOptions): Promise<OmpSessionDescriptor[]>;
1386
+ readPersistedSessionTranscript?(options: {
1387
+ sessionFile: string;
1388
+ sessionId: string;
1389
+ cwd: string;
1390
+ signal?: AbortSignal;
1391
+ }): Promise<OmpPersistedSessionMessages>;
1025
1392
  readPersistedSubagentTranscript(options: {
1026
1393
  parentSessionFile: string;
1027
1394
  childTranscriptId: string;
@@ -1965,21 +2332,7 @@ class OmpRpcProcess {
1965
2332
  this.recordProtocolViolation();
1966
2333
  return;
1967
2334
  }
1968
- if (this.receiveKnownResponse(decoded)) return;
1969
- if (this.receiveDegradedAgentEnd(decoded, true)) return;
1970
- if (
1971
- boundedJsonBytes(decoded, MAX_SEMANTIC_FRAME_BYTES, 1_024, MAX_IMAGE_DATA_LENGTH, 4_096) ===
1972
- Number.POSITIVE_INFINITY
1973
- ) {
1974
- this.recordProtocolViolation();
1975
- return;
1976
- }
1977
- const frame = JsonObjectSchema.safeParse(decoded);
1978
- if (!frame.success) {
1979
- this.recordProtocolViolation();
1980
- return;
1981
- }
1982
- this.receiveFrame(frame.data);
2335
+ this.receiveDecodedFrame(decoded, payload.byteLength);
1983
2336
  }
1984
2337
 
1985
2338
  private receiveChunk(frame: ChunkFrame): void {
@@ -2049,32 +2402,43 @@ class OmpRpcProcess {
2049
2402
  this.recordProtocolViolation();
2050
2403
  return;
2051
2404
  }
2052
- if (this.receiveKnownResponse(decodedFrame)) return;
2053
- if (this.receiveDegradedAgentEnd(decodedFrame, true)) return;
2054
- if (
2055
- boundedJsonBytes(
2056
- decodedFrame,
2057
- MAX_SEMANTIC_FRAME_BYTES,
2058
- 1_024,
2059
- MAX_IMAGE_DATA_LENGTH,
2060
- 4_096,
2061
- ) === Number.POSITIVE_INFINITY
2062
- ) {
2063
- this.recordProtocolViolation();
2405
+ this.receiveDecodedFrame(decodedFrame, reassembled.byteLength);
2406
+ }
2407
+
2408
+ private receiveDecodedFrame(value: unknown, rawByteLength: number): void {
2409
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2410
+ const frame = value as Record<string, unknown>;
2411
+ const pending = typeof frame.id === "string" ? this.pending.get(frame.id) : undefined;
2412
+ if (
2413
+ frame.type === "response" &&
2414
+ (pending?.command === "get_messages" || pending?.command === "get_subagent_messages")
2415
+ ) {
2416
+ this.receiveResponse(frame);
2417
+ return;
2418
+ }
2419
+ }
2420
+ if (rawByteLength > MAX_SEMANTIC_FRAME_BYTES) {
2421
+ this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
2064
2422
  return;
2065
2423
  }
2066
- const frameObject = JsonObjectSchema.safeParse(decodedFrame);
2067
- if (!frameObject.success) {
2424
+ if (this.receiveKnownResponse(value)) return;
2425
+ const sanitized = sanitizeLiveDisplayFrame(value);
2426
+ const frame = JsonObjectSchema.safeParse(sanitized);
2427
+ if (!frame.success) {
2068
2428
  this.recordProtocolViolation();
2069
2429
  return;
2070
2430
  }
2071
- this.receiveFrame(frameObject.data);
2431
+ this.receiveFrame(frame.data);
2072
2432
  }
2073
2433
 
2074
2434
  private receiveKnownResponse(value: unknown): boolean {
2075
2435
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2076
2436
  const frame = value as Record<string, unknown>;
2077
- if (frame.type !== "response" || typeof frame.id !== "string" || !this.pending.has(frame.id)) {
2437
+ if (
2438
+ frame.type !== "response" ||
2439
+ typeof frame.id !== "string" ||
2440
+ (!this.pending.has(frame.id) && !this.acceptedPromptIds.has(frame.id))
2441
+ ) {
2078
2442
  return false;
2079
2443
  }
2080
2444
  this.receiveResponse(frame);
@@ -2088,34 +2452,43 @@ class OmpRpcProcess {
2088
2452
  if (!response.success) {
2089
2453
  if (rawId && knownPending) {
2090
2454
  this.takePending(rawId)?.reject(new Error("OMP RPC response is invalid"));
2091
- } else {
2455
+ } else if (!rawId || !this.emitAcceptedPromptFailure(rawId, frame)) {
2092
2456
  this.recordProtocolViolation();
2093
2457
  }
2094
2458
  return;
2095
2459
  }
2096
2460
  const pending = this.pending.get(response.data.id);
2097
2461
  if (!pending) {
2098
- if (!response.data.success && this.acceptedPromptIds.delete(response.data.id)) {
2099
- this.emit({
2100
- type: "prompt_error",
2101
- id: response.data.id,
2102
- error: "OMP prompt scheduling failed",
2103
- });
2462
+ if (!response.data.success) {
2463
+ this.emitAcceptedPromptFailure(response.data.id, response.data);
2104
2464
  }
2105
2465
  return;
2106
2466
  }
2107
2467
  const isBranchHistory = pending.command === "get_branch_messages";
2108
2468
  const isHistory =
2109
2469
  pending.command === "get_messages" || pending.command === "get_subagent_messages";
2470
+ const responseData = isHistory
2471
+ ? sanitizeHistoryResponseData(response.data.data)
2472
+ : response.data.data;
2473
+ const boundedFrame =
2474
+ responseData === response.data.data ? frame : { ...frame, data: responseData };
2110
2475
  const responseItemLimit = isBranchHistory ? 1_024 : isHistory ? 100_000 : MAX_ARRAY_ITEMS;
2111
2476
  const responseByteLimit =
2112
2477
  isBranchHistory || isHistory
2113
2478
  ? Math.min(MAX_REASSEMBLED_FRAME_BYTES, this.reassembledFrameLimit)
2114
2479
  : 2 * 1024 * 1024;
2115
- const responseNodeLimit = isBranchHistory ? 4_096 : isHistory ? 400_000 : 2_048;
2480
+ // A model catalog contains up to 256 structured models, so its aggregate
2481
+ // node budget must exceed the small state/command-response budget.
2482
+ const responseNodeLimit = isBranchHistory
2483
+ ? 4_096
2484
+ : isHistory
2485
+ ? 400_000
2486
+ : pending.command === "get_available_models"
2487
+ ? 16_384
2488
+ : 2_048;
2116
2489
  if (
2117
2490
  boundedJsonBytes(
2118
- frame,
2491
+ boundedFrame,
2119
2492
  responseByteLimit,
2120
2493
  responseItemLimit,
2121
2494
  MAX_IMAGE_DATA_LENGTH,
@@ -2131,7 +2504,7 @@ class OmpRpcProcess {
2131
2504
  if (!settled) return;
2132
2505
  if (response.data.success) {
2133
2506
  try {
2134
- settled.beforeResolve?.(response.data.data);
2507
+ settled.beforeResolve?.(responseData);
2135
2508
  if (settled.command === "prompt") {
2136
2509
  if (this.acceptedPromptIds.size >= MAX_PENDING_REQUESTS) {
2137
2510
  const oldest = this.acceptedPromptIds.values().next().value;
@@ -2139,7 +2512,7 @@ class OmpRpcProcess {
2139
2512
  }
2140
2513
  this.acceptedPromptIds.add(response.data.id);
2141
2514
  }
2142
- settled.resolve(response.data.data);
2515
+ settled.resolve(responseData);
2143
2516
  } catch {
2144
2517
  settled.reject(new Error("OMP RPC response is invalid"));
2145
2518
  }
@@ -2148,6 +2521,22 @@ class OmpRpcProcess {
2148
2521
  }
2149
2522
  }
2150
2523
 
2524
+ private emitAcceptedPromptFailure(id: string, frame: Record<string, unknown>): boolean {
2525
+ if (frame.success !== false || !this.acceptedPromptIds.delete(id)) return false;
2526
+ const error = typeof frame.error === "string" ? frame.error : undefined;
2527
+ const nativeError =
2528
+ error && utf8Bytes(error) <= MAX_RPC_ERROR_BYTES ? error : PROMPT_SCHEDULING_FAILURE;
2529
+ const code = typeof frame.code === "string" ? frame.code : undefined;
2530
+ const nativeCode = code && utf8Bytes(code) <= MAX_RPC_ERROR_CODE_BYTES ? code : undefined;
2531
+ this.emit({
2532
+ type: "prompt_error",
2533
+ id,
2534
+ error: nativeError,
2535
+ ...(nativeCode ? { code: nativeCode } : {}),
2536
+ });
2537
+ return true;
2538
+ }
2539
+
2151
2540
  private takePending(id: string): PendingRequest | undefined {
2152
2541
  const pending = this.pending.get(id);
2153
2542
  if (!pending) return undefined;
@@ -2172,21 +2561,27 @@ class OmpRpcProcess {
2172
2561
  this.fail(new Error("OMP emitted invalid terminal metadata"));
2173
2562
  return true;
2174
2563
  }
2564
+ const structuralFrame = mapRuntimeFrameDetails(frame, omitOptionalDetails);
2175
2565
  const messagesAreSafe =
2176
2566
  frame.messages === undefined ||
2177
2567
  (Array.isArray(frame.messages) &&
2178
2568
  frame.messages.length <= MAX_ARRAY_ITEMS &&
2179
2569
  boundedJsonBytes(
2180
- frame.messages,
2570
+ structuralFrame.messages as unknown[],
2181
2571
  MAX_SEMANTIC_FRAME_BYTES,
2182
- MAX_ARRAY_ITEMS,
2183
- MAX_TEXT_LENGTH,
2572
+ OMP_MAX_CONTENT_PARTS,
2573
+ MAX_IMAGE_DATA_LENGTH,
2184
2574
  4_096,
2185
2575
  ) !== Number.POSITIVE_INFINITY);
2186
2576
  const payloadIsSafe =
2187
2577
  messagesAreSafe &&
2188
- boundedJsonBytes(frame, MAX_SEMANTIC_FRAME_BYTES, 1_024, MAX_IMAGE_DATA_LENGTH, 4_096) !==
2189
- Number.POSITIVE_INFINITY;
2578
+ boundedJsonBytes(
2579
+ structuralFrame,
2580
+ MAX_SEMANTIC_FRAME_BYTES,
2581
+ OMP_MAX_CONTENT_PARTS,
2582
+ MAX_IMAGE_DATA_LENGTH,
2583
+ 4_096,
2584
+ ) !== Number.POSITIVE_INFINITY;
2190
2585
  if (onlyUnsafePayload && payloadIsSafe) return false;
2191
2586
  if (envelope.data.isTerminal === false) {
2192
2587
  this.fail(new Error("OMP emitted an invalid nonterminal agent_end payload"));
@@ -2213,16 +2608,22 @@ class OmpRpcProcess {
2213
2608
  this.recordProtocolViolation();
2214
2609
  return;
2215
2610
  }
2216
- if (this.receiveDegradedAgentEnd(frame, true)) return;
2611
+ const safeFrame = mapRuntimeFrameDetails(frame, createOptionalMetadataSanitizer());
2612
+ if (this.receiveDegradedAgentEnd(safeFrame, true)) return;
2217
2613
  if (
2218
- boundedJsonBytes(frame, MAX_SEMANTIC_FRAME_BYTES, 1_024, MAX_IMAGE_DATA_LENGTH, 4_096) ===
2219
- Number.POSITIVE_INFINITY
2614
+ boundedJsonBytes(
2615
+ mapRuntimeFrameDetails(safeFrame, omitOptionalDetails),
2616
+ MAX_SEMANTIC_FRAME_BYTES,
2617
+ runtimeFrameCollectionLimit(safeFrame),
2618
+ MAX_IMAGE_DATA_LENGTH,
2619
+ 4_096,
2620
+ ) === Number.POSITIVE_INFINITY
2220
2621
  ) {
2221
2622
  this.recordProtocolViolation();
2222
2623
  return;
2223
2624
  }
2224
2625
  if (type === "rpc_chunk") {
2225
- const chunk = OmpChunkFrameSchema.safeParse(frame);
2626
+ const chunk = OmpChunkFrameSchema.safeParse(safeFrame);
2226
2627
  if (!chunk.success) this.rejectChunk();
2227
2628
  else this.receiveChunk(chunk.data);
2228
2629
  return;
@@ -2240,7 +2641,7 @@ class OmpRpcProcess {
2240
2641
  this.recordProtocolViolation();
2241
2642
  return;
2242
2643
  }
2243
- const ready = OmpReadyFrameSchema.safeParse(frame);
2644
+ const ready = OmpReadyFrameSchema.safeParse(safeFrame);
2244
2645
  if (!ready.success) {
2245
2646
  this.recordProtocolViolation();
2246
2647
  } else {
@@ -2250,13 +2651,13 @@ class OmpRpcProcess {
2250
2651
  return;
2251
2652
  }
2252
2653
  if (type === "response") {
2253
- this.receiveResponse(frame);
2654
+ this.receiveResponse(safeFrame);
2254
2655
  return;
2255
2656
  }
2256
- const event = OmpRuntimeEventSchema.safeParse(frame);
2657
+ const event = OmpRuntimeEventSchema.safeParse(safeFrame);
2257
2658
  if (!event.success) {
2258
- this.rejectMatchingToolApproval(frame);
2259
- if (type === "agent_end" && this.receiveDegradedAgentEnd(frame, false)) return;
2659
+ this.rejectMatchingToolApproval(safeFrame);
2660
+ if (type === "agent_end" && this.receiveDegradedAgentEnd(safeFrame, false)) return;
2260
2661
  this.recordProtocolViolation();
2261
2662
  return;
2262
2663
  }
@@ -2595,6 +2996,7 @@ class OmpRpcSession implements OmpRuntimeSession {
2595
2996
  message: string,
2596
2997
  images: readonly OmpImage[] = [],
2597
2998
  onAccepted?: () => void,
2999
+ onRequested?: (requestId: string) => void,
2598
3000
  ): Promise<{ requestId: string; agentInvoked?: boolean }> {
2599
3001
  const safeMessage = validateBoundedText(message, "prompt", MAX_TEXT_LENGTH);
2600
3002
  let acknowledgement: z.infer<typeof OmpPromptAckSchema> | undefined;
@@ -2606,6 +3008,7 @@ class OmpRpcSession implements OmpRuntimeSession {
2606
3008
  onAccepted?.();
2607
3009
  },
2608
3010
  );
3011
+ onRequested?.(request.id);
2609
3012
  await request.promise;
2610
3013
  return { requestId: request.id, ...acknowledgement };
2611
3014
  }
@@ -2664,6 +3067,27 @@ export class OmpRpcRuntime implements OmpRuntime {
2664
3067
  listOmpSessionDescriptors(options, this.options.environment ?? process.env),
2665
3068
  );
2666
3069
  }
3070
+ async readPersistedSessionTranscript(options: {
3071
+ sessionFile: string;
3072
+ sessionId: string;
3073
+ cwd: string;
3074
+ signal?: AbortSignal;
3075
+ }): Promise<OmpPersistedSessionMessages> {
3076
+ const transcript = await readOmpPersistedSessionTranscript(
3077
+ options.sessionFile,
3078
+ options.sessionId,
3079
+ options.cwd,
3080
+ options.signal,
3081
+ join(ompDataDir(this.options.environment ?? process.env), "blobs"),
3082
+ );
3083
+ return {
3084
+ ...transcript,
3085
+ messages: z
3086
+ .array(OmpMessageSchema)
3087
+ .max(100_000)
3088
+ .parse(sanitizeMessageListMetadata(transcript.messages)),
3089
+ };
3090
+ }
2667
3091
  async readPersistedSubagentTranscript(options: {
2668
3092
  parentSessionFile: string;
2669
3093
  childTranscriptId: string;
@@ -2678,7 +3102,10 @@ export class OmpRpcRuntime implements OmpRuntime {
2678
3102
  );
2679
3103
  return {
2680
3104
  ...transcript,
2681
- messages: z.array(OmpMessageSchema).max(100_000).parse(transcript.messages),
3105
+ messages: z
3106
+ .array(OmpMessageSchema)
3107
+ .max(100_000)
3108
+ .parse(sanitizeMessageListMetadata(transcript.messages)),
2682
3109
  };
2683
3110
  }
2684
3111