@omercnet/paseo-omp 0.2.1 → 0.3.0-next.100.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 +498 -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 +306 -1
  47. package/server/provider/session.ts +704 -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,12 @@ 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
+ }
948
1307
 
949
1308
  export interface OmpStartOptions {
950
1309
  cwd: string;
@@ -997,6 +1356,7 @@ export interface OmpRuntimeSession {
997
1356
  message: string,
998
1357
  images?: readonly OmpImage[],
999
1358
  onAccepted?: () => void,
1359
+ onRequested?: (requestId: string) => void,
1000
1360
  ): Promise<{ requestId: string; agentInvoked?: boolean }>;
1001
1361
  compact(customInstructions?: string): Promise<OmpCompactionResult>;
1002
1362
  setAutoCompaction(enabled: boolean): Promise<void>;
@@ -1022,6 +1382,12 @@ export interface OmpRuntime {
1022
1382
  readonly supportsPersistence: boolean;
1023
1383
  startSession(options: OmpStartOptions): Promise<OmpRuntimeSession>;
1024
1384
  listSessions(options: OmpSessionListOptions): Promise<OmpSessionDescriptor[]>;
1385
+ readPersistedSessionTranscript?(options: {
1386
+ sessionFile: string;
1387
+ sessionId: string;
1388
+ cwd: string;
1389
+ signal?: AbortSignal;
1390
+ }): Promise<OmpPersistedSessionMessages>;
1025
1391
  readPersistedSubagentTranscript(options: {
1026
1392
  parentSessionFile: string;
1027
1393
  childTranscriptId: string;
@@ -1965,21 +2331,7 @@ class OmpRpcProcess {
1965
2331
  this.recordProtocolViolation();
1966
2332
  return;
1967
2333
  }
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);
2334
+ this.receiveDecodedFrame(decoded, payload.byteLength);
1983
2335
  }
1984
2336
 
1985
2337
  private receiveChunk(frame: ChunkFrame): void {
@@ -2049,32 +2401,43 @@ class OmpRpcProcess {
2049
2401
  this.recordProtocolViolation();
2050
2402
  return;
2051
2403
  }
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();
2404
+ this.receiveDecodedFrame(decodedFrame, reassembled.byteLength);
2405
+ }
2406
+
2407
+ private receiveDecodedFrame(value: unknown, rawByteLength: number): void {
2408
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2409
+ const frame = value as Record<string, unknown>;
2410
+ const pending = typeof frame.id === "string" ? this.pending.get(frame.id) : undefined;
2411
+ if (
2412
+ frame.type === "response" &&
2413
+ (pending?.command === "get_messages" || pending?.command === "get_subagent_messages")
2414
+ ) {
2415
+ this.receiveResponse(frame);
2416
+ return;
2417
+ }
2418
+ }
2419
+ if (rawByteLength > MAX_SEMANTIC_FRAME_BYTES) {
2420
+ this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
2064
2421
  return;
2065
2422
  }
2066
- const frameObject = JsonObjectSchema.safeParse(decodedFrame);
2067
- if (!frameObject.success) {
2423
+ if (this.receiveKnownResponse(value)) return;
2424
+ const sanitized = sanitizeLiveDisplayFrame(value);
2425
+ const frame = JsonObjectSchema.safeParse(sanitized);
2426
+ if (!frame.success) {
2068
2427
  this.recordProtocolViolation();
2069
2428
  return;
2070
2429
  }
2071
- this.receiveFrame(frameObject.data);
2430
+ this.receiveFrame(frame.data);
2072
2431
  }
2073
2432
 
2074
2433
  private receiveKnownResponse(value: unknown): boolean {
2075
2434
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2076
2435
  const frame = value as Record<string, unknown>;
2077
- if (frame.type !== "response" || typeof frame.id !== "string" || !this.pending.has(frame.id)) {
2436
+ if (
2437
+ frame.type !== "response" ||
2438
+ typeof frame.id !== "string" ||
2439
+ (!this.pending.has(frame.id) && !this.acceptedPromptIds.has(frame.id))
2440
+ ) {
2078
2441
  return false;
2079
2442
  }
2080
2443
  this.receiveResponse(frame);
@@ -2088,34 +2451,43 @@ class OmpRpcProcess {
2088
2451
  if (!response.success) {
2089
2452
  if (rawId && knownPending) {
2090
2453
  this.takePending(rawId)?.reject(new Error("OMP RPC response is invalid"));
2091
- } else {
2454
+ } else if (!rawId || !this.emitAcceptedPromptFailure(rawId, frame)) {
2092
2455
  this.recordProtocolViolation();
2093
2456
  }
2094
2457
  return;
2095
2458
  }
2096
2459
  const pending = this.pending.get(response.data.id);
2097
2460
  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
- });
2461
+ if (!response.data.success) {
2462
+ this.emitAcceptedPromptFailure(response.data.id, response.data);
2104
2463
  }
2105
2464
  return;
2106
2465
  }
2107
2466
  const isBranchHistory = pending.command === "get_branch_messages";
2108
2467
  const isHistory =
2109
2468
  pending.command === "get_messages" || pending.command === "get_subagent_messages";
2469
+ const responseData = isHistory
2470
+ ? sanitizeHistoryResponseData(response.data.data)
2471
+ : response.data.data;
2472
+ const boundedFrame =
2473
+ responseData === response.data.data ? frame : { ...frame, data: responseData };
2110
2474
  const responseItemLimit = isBranchHistory ? 1_024 : isHistory ? 100_000 : MAX_ARRAY_ITEMS;
2111
2475
  const responseByteLimit =
2112
2476
  isBranchHistory || isHistory
2113
2477
  ? Math.min(MAX_REASSEMBLED_FRAME_BYTES, this.reassembledFrameLimit)
2114
2478
  : 2 * 1024 * 1024;
2115
- const responseNodeLimit = isBranchHistory ? 4_096 : isHistory ? 400_000 : 2_048;
2479
+ // A model catalog contains up to 256 structured models, so its aggregate
2480
+ // node budget must exceed the small state/command-response budget.
2481
+ const responseNodeLimit = isBranchHistory
2482
+ ? 4_096
2483
+ : isHistory
2484
+ ? 400_000
2485
+ : pending.command === "get_available_models"
2486
+ ? 16_384
2487
+ : 2_048;
2116
2488
  if (
2117
2489
  boundedJsonBytes(
2118
- frame,
2490
+ boundedFrame,
2119
2491
  responseByteLimit,
2120
2492
  responseItemLimit,
2121
2493
  MAX_IMAGE_DATA_LENGTH,
@@ -2131,7 +2503,7 @@ class OmpRpcProcess {
2131
2503
  if (!settled) return;
2132
2504
  if (response.data.success) {
2133
2505
  try {
2134
- settled.beforeResolve?.(response.data.data);
2506
+ settled.beforeResolve?.(responseData);
2135
2507
  if (settled.command === "prompt") {
2136
2508
  if (this.acceptedPromptIds.size >= MAX_PENDING_REQUESTS) {
2137
2509
  const oldest = this.acceptedPromptIds.values().next().value;
@@ -2139,7 +2511,7 @@ class OmpRpcProcess {
2139
2511
  }
2140
2512
  this.acceptedPromptIds.add(response.data.id);
2141
2513
  }
2142
- settled.resolve(response.data.data);
2514
+ settled.resolve(responseData);
2143
2515
  } catch {
2144
2516
  settled.reject(new Error("OMP RPC response is invalid"));
2145
2517
  }
@@ -2148,6 +2520,22 @@ class OmpRpcProcess {
2148
2520
  }
2149
2521
  }
2150
2522
 
2523
+ private emitAcceptedPromptFailure(id: string, frame: Record<string, unknown>): boolean {
2524
+ if (frame.success !== false || !this.acceptedPromptIds.delete(id)) return false;
2525
+ const error = typeof frame.error === "string" ? frame.error : undefined;
2526
+ const nativeError =
2527
+ error && utf8Bytes(error) <= MAX_RPC_ERROR_BYTES ? error : PROMPT_SCHEDULING_FAILURE;
2528
+ const code = typeof frame.code === "string" ? frame.code : undefined;
2529
+ const nativeCode = code && utf8Bytes(code) <= MAX_RPC_ERROR_CODE_BYTES ? code : undefined;
2530
+ this.emit({
2531
+ type: "prompt_error",
2532
+ id,
2533
+ error: nativeError,
2534
+ ...(nativeCode ? { code: nativeCode } : {}),
2535
+ });
2536
+ return true;
2537
+ }
2538
+
2151
2539
  private takePending(id: string): PendingRequest | undefined {
2152
2540
  const pending = this.pending.get(id);
2153
2541
  if (!pending) return undefined;
@@ -2172,21 +2560,27 @@ class OmpRpcProcess {
2172
2560
  this.fail(new Error("OMP emitted invalid terminal metadata"));
2173
2561
  return true;
2174
2562
  }
2563
+ const structuralFrame = mapRuntimeFrameDetails(frame, omitOptionalDetails);
2175
2564
  const messagesAreSafe =
2176
2565
  frame.messages === undefined ||
2177
2566
  (Array.isArray(frame.messages) &&
2178
2567
  frame.messages.length <= MAX_ARRAY_ITEMS &&
2179
2568
  boundedJsonBytes(
2180
- frame.messages,
2569
+ structuralFrame.messages as unknown[],
2181
2570
  MAX_SEMANTIC_FRAME_BYTES,
2182
- MAX_ARRAY_ITEMS,
2183
- MAX_TEXT_LENGTH,
2571
+ OMP_MAX_CONTENT_PARTS,
2572
+ MAX_IMAGE_DATA_LENGTH,
2184
2573
  4_096,
2185
2574
  ) !== Number.POSITIVE_INFINITY);
2186
2575
  const payloadIsSafe =
2187
2576
  messagesAreSafe &&
2188
- boundedJsonBytes(frame, MAX_SEMANTIC_FRAME_BYTES, 1_024, MAX_IMAGE_DATA_LENGTH, 4_096) !==
2189
- Number.POSITIVE_INFINITY;
2577
+ boundedJsonBytes(
2578
+ structuralFrame,
2579
+ MAX_SEMANTIC_FRAME_BYTES,
2580
+ OMP_MAX_CONTENT_PARTS,
2581
+ MAX_IMAGE_DATA_LENGTH,
2582
+ 4_096,
2583
+ ) !== Number.POSITIVE_INFINITY;
2190
2584
  if (onlyUnsafePayload && payloadIsSafe) return false;
2191
2585
  if (envelope.data.isTerminal === false) {
2192
2586
  this.fail(new Error("OMP emitted an invalid nonterminal agent_end payload"));
@@ -2213,16 +2607,22 @@ class OmpRpcProcess {
2213
2607
  this.recordProtocolViolation();
2214
2608
  return;
2215
2609
  }
2216
- if (this.receiveDegradedAgentEnd(frame, true)) return;
2610
+ const safeFrame = mapRuntimeFrameDetails(frame, createOptionalMetadataSanitizer());
2611
+ if (this.receiveDegradedAgentEnd(safeFrame, true)) return;
2217
2612
  if (
2218
- boundedJsonBytes(frame, MAX_SEMANTIC_FRAME_BYTES, 1_024, MAX_IMAGE_DATA_LENGTH, 4_096) ===
2219
- Number.POSITIVE_INFINITY
2613
+ boundedJsonBytes(
2614
+ mapRuntimeFrameDetails(safeFrame, omitOptionalDetails),
2615
+ MAX_SEMANTIC_FRAME_BYTES,
2616
+ runtimeFrameCollectionLimit(safeFrame),
2617
+ MAX_IMAGE_DATA_LENGTH,
2618
+ 4_096,
2619
+ ) === Number.POSITIVE_INFINITY
2220
2620
  ) {
2221
2621
  this.recordProtocolViolation();
2222
2622
  return;
2223
2623
  }
2224
2624
  if (type === "rpc_chunk") {
2225
- const chunk = OmpChunkFrameSchema.safeParse(frame);
2625
+ const chunk = OmpChunkFrameSchema.safeParse(safeFrame);
2226
2626
  if (!chunk.success) this.rejectChunk();
2227
2627
  else this.receiveChunk(chunk.data);
2228
2628
  return;
@@ -2240,7 +2640,7 @@ class OmpRpcProcess {
2240
2640
  this.recordProtocolViolation();
2241
2641
  return;
2242
2642
  }
2243
- const ready = OmpReadyFrameSchema.safeParse(frame);
2643
+ const ready = OmpReadyFrameSchema.safeParse(safeFrame);
2244
2644
  if (!ready.success) {
2245
2645
  this.recordProtocolViolation();
2246
2646
  } else {
@@ -2250,13 +2650,13 @@ class OmpRpcProcess {
2250
2650
  return;
2251
2651
  }
2252
2652
  if (type === "response") {
2253
- this.receiveResponse(frame);
2653
+ this.receiveResponse(safeFrame);
2254
2654
  return;
2255
2655
  }
2256
- const event = OmpRuntimeEventSchema.safeParse(frame);
2656
+ const event = OmpRuntimeEventSchema.safeParse(safeFrame);
2257
2657
  if (!event.success) {
2258
- this.rejectMatchingToolApproval(frame);
2259
- if (type === "agent_end" && this.receiveDegradedAgentEnd(frame, false)) return;
2658
+ this.rejectMatchingToolApproval(safeFrame);
2659
+ if (type === "agent_end" && this.receiveDegradedAgentEnd(safeFrame, false)) return;
2260
2660
  this.recordProtocolViolation();
2261
2661
  return;
2262
2662
  }
@@ -2595,6 +2995,7 @@ class OmpRpcSession implements OmpRuntimeSession {
2595
2995
  message: string,
2596
2996
  images: readonly OmpImage[] = [],
2597
2997
  onAccepted?: () => void,
2998
+ onRequested?: (requestId: string) => void,
2598
2999
  ): Promise<{ requestId: string; agentInvoked?: boolean }> {
2599
3000
  const safeMessage = validateBoundedText(message, "prompt", MAX_TEXT_LENGTH);
2600
3001
  let acknowledgement: z.infer<typeof OmpPromptAckSchema> | undefined;
@@ -2606,6 +3007,7 @@ class OmpRpcSession implements OmpRuntimeSession {
2606
3007
  onAccepted?.();
2607
3008
  },
2608
3009
  );
3010
+ onRequested?.(request.id);
2609
3011
  await request.promise;
2610
3012
  return { requestId: request.id, ...acknowledgement };
2611
3013
  }
@@ -2664,6 +3066,27 @@ export class OmpRpcRuntime implements OmpRuntime {
2664
3066
  listOmpSessionDescriptors(options, this.options.environment ?? process.env),
2665
3067
  );
2666
3068
  }
3069
+ async readPersistedSessionTranscript(options: {
3070
+ sessionFile: string;
3071
+ sessionId: string;
3072
+ cwd: string;
3073
+ signal?: AbortSignal;
3074
+ }): Promise<OmpPersistedSessionMessages> {
3075
+ const transcript = await readOmpPersistedSessionTranscript(
3076
+ options.sessionFile,
3077
+ options.sessionId,
3078
+ options.cwd,
3079
+ options.signal,
3080
+ join(ompDataDir(this.options.environment ?? process.env), "blobs"),
3081
+ );
3082
+ return {
3083
+ ...transcript,
3084
+ messages: z
3085
+ .array(OmpMessageSchema)
3086
+ .max(100_000)
3087
+ .parse(sanitizeMessageListMetadata(transcript.messages)),
3088
+ };
3089
+ }
2667
3090
  async readPersistedSubagentTranscript(options: {
2668
3091
  parentSessionFile: string;
2669
3092
  childTranscriptId: string;
@@ -2678,7 +3101,10 @@ export class OmpRpcRuntime implements OmpRuntime {
2678
3101
  );
2679
3102
  return {
2680
3103
  ...transcript,
2681
- messages: z.array(OmpMessageSchema).max(100_000).parse(transcript.messages),
3104
+ messages: z
3105
+ .array(OmpMessageSchema)
3106
+ .max(100_000)
3107
+ .parse(sanitizeMessageListMetadata(transcript.messages)),
2682
3108
  };
2683
3109
  }
2684
3110