@omercnet/paseo-omp 0.3.0 → 0.4.0-next.119.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +19 -5
  2. package/client/omp-config-surface.tsx +243 -24
  3. package/client/omp-config-views.ts +24 -0
  4. package/client/omp-model-picker-state.ts +145 -0
  5. package/client/omp-model-picker.tsx +282 -0
  6. package/client/omp-routing-editor.tsx +307 -0
  7. package/client/support-diagnostics-state.ts +45 -0
  8. package/index.server.ts +27 -2
  9. package/package.json +4 -8
  10. package/paseo-plugin.json +1 -1
  11. package/server/omp-models.ts +59 -0
  12. package/server/omp-settings.ts +30 -20
  13. package/server/operational-failure-diagnostics.ts +76 -0
  14. package/server/package-version.ts +2 -0
  15. package/server/protocol-violation-diagnostics.ts +169 -0
  16. package/server/provider/catalog.ts +39 -10
  17. package/server/provider/connection.ts +88 -9
  18. package/server/provider/host-tools.ts +284 -34
  19. package/server/provider/mcp-transport.ts +2 -1
  20. package/server/provider/omp-rpc.ts +1785 -267
  21. package/server/provider/profile-providers.ts +7 -2
  22. package/server/provider/registration.ts +12 -2
  23. package/server/provider/security.ts +8 -10
  24. package/server/provider/session-descriptors.ts +97 -29
  25. package/server/provider/session.ts +212 -59
  26. package/server/provider/subsessions.ts +334 -74
  27. package/server/provider/timeline-projector.ts +125 -32
  28. package/server/support-diagnostics.ts +284 -0
  29. package/shared/omp-models.ts +49 -0
  30. package/shared/omp-settings.ts +227 -3
  31. package/shared/support-diagnostics.ts +32 -0
  32. package/CHANGELOG.md +0 -113
  33. package/SUPPORT.md +0 -44
  34. package/TESTING.md +0 -150
  35. package/docs/alpha-release-checklist.md +0 -68
  36. package/docs/configuration.md +0 -126
  37. package/docs/core-provider-issue-audit.md +0 -109
  38. package/docs/images/mcp-authorization-compact.png +0 -0
  39. package/docs/images/mcp-controls-wide.png +0 -0
  40. package/docs/images/plugin-manager.png +0 -0
  41. package/docs/images/workspace-settings.png +0 -0
  42. package/docs/installation.md +0 -89
  43. package/tsconfig.json +0 -16
@@ -6,17 +6,25 @@ import type {
6
6
  } from "@getpaseo/plugin/server/provider";
7
7
  import { OMP_MCP_AUTH_TIMELINE_KIND } from "../../shared/mcp";
8
8
  import { isOmpImageMimeType, isValidImagePayload, type OmpImageMimeType } from "./image";
9
- import { OMP_MAX_CONTENT_PARTS, type OmpMessage, type OmpRpcEvent } from "./omp-rpc";
9
+ import {
10
+ OMP_MAX_CONTENT_PARTS,
11
+ OMP_MESSAGE_REPLAY_POLICIES,
12
+ type OmpMessage,
13
+ type OmpRpcEvent,
14
+ } from "./omp-rpc";
10
15
  import {
11
16
  boundedJsonBytes,
12
17
  type JsonValue,
13
18
  OmpPublicDataSerializer,
14
19
  OmpPublicError,
20
+ truncateUtf8,
15
21
  utf8Bytes,
16
22
  } from "./security";
17
23
 
18
24
  const STREAM_FRAME_MS = 32;
19
25
  const MAX_STREAM_TEXT_LENGTH = 4 * 1024 * 1024;
26
+ const MAX_RAW_STREAM_TEXT_LENGTH = 8 * 1024 * 1024;
27
+ const MAX_REDACTED_STREAM_TEXT_LENGTH = MAX_RAW_STREAM_TEXT_LENGTH * 3;
20
28
  const MAX_ACTIVE_TOOLS = 64;
21
29
  const MAX_TODOS = 256;
22
30
  const MAX_TURN_NATIVE_IDENTITIES = 1_024;
@@ -131,6 +139,10 @@ function imageBlock(data: string, mimeType: string): StreamBlockSnapshot | undef
131
139
  }
132
140
 
133
141
  type OmpAssistantMessage = Extract<OmpMessage, { role: "assistant" }>;
142
+ type OmpCustomDisplayMessage = Extract<
143
+ OmpMessage,
144
+ { role: "bashExecution" | "pythonExecution" | "custom" | "hookMessage" }
145
+ >;
134
146
 
135
147
  function blockText(
136
148
  message: OmpAssistantMessage,
@@ -414,16 +426,35 @@ export class OmpTimelineProjector {
414
426
  return;
415
427
  }
416
428
  }
429
+ if (
430
+ (event.type === "message_start" ||
431
+ event.type === "message_update" ||
432
+ event.type === "message_end") &&
433
+ OMP_MESSAGE_REPLAY_POLICIES[event.message.role] === "ignore"
434
+ ) {
435
+ return;
436
+ }
417
437
  if (
418
438
  event.type === "todo_reminder" ||
419
439
  event.type === "todo_auto_clear" ||
440
+ event.type === "goal_updated" ||
441
+ event.type === "auto_retry_start" ||
442
+ event.type === "auto_retry_end" ||
443
+ event.type === "retry_fallback_applied" ||
444
+ event.type === "retry_fallback_succeeded" ||
420
445
  event.type === "notice" ||
421
446
  event.type === "extension_ui_request" ||
422
447
  event.type === "auto_compaction_start" ||
423
448
  event.type === "auto_compaction_end" ||
424
449
  event.type === "compaction_start" ||
425
450
  event.type === "compaction_end" ||
426
- event.type === "advisor_yielded"
451
+ event.type === "advisor_yielded" ||
452
+ event.type === "config_warnings_changed" ||
453
+ event.type === "advisor_cost_changed" ||
454
+ event.type === "ttsr_triggered" ||
455
+ event.type === "irc_message" ||
456
+ event.type === "model_changed" ||
457
+ event.type === "thinking_level_changed"
427
458
  ) {
428
459
  this.projectPassive(event);
429
460
  return;
@@ -446,7 +477,12 @@ export class OmpTimelineProjector {
446
477
  this.scheduleFlush();
447
478
  return;
448
479
  case "message_end":
449
- if (event.message.role === "custom" || event.message.role === "bashExecution") {
480
+ if (
481
+ event.message.role === "custom" ||
482
+ event.message.role === "hookMessage" ||
483
+ event.message.role === "bashExecution" ||
484
+ event.message.role === "pythonExecution"
485
+ ) {
450
486
  this.publishCustomMessage(event.message);
451
487
  return;
452
488
  }
@@ -496,12 +532,13 @@ export class OmpTimelineProjector {
496
532
  if (!snapshot.silent) this.publishTool(snapshot, "running");
497
533
  return;
498
534
  }
499
- case "tool_execution_update": {
535
+ case "tool_execution_update":
536
+ case "tool_stream_update": {
500
537
  const previous = this.tools.get(event.toolCallId);
501
538
  if (!previous) return;
502
539
  if (previous.turnId !== turnId || previous.generation !== this.runtimeGeneration) return;
503
540
  const output = this.dataFilter.json(
504
- event.partialResult,
541
+ event.type === "tool_execution_update" ? event.partialResult : event.update,
505
542
  MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
506
543
  MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
507
544
  );
@@ -597,6 +634,10 @@ export class OmpTimelineProjector {
597
634
  this.publishRetryFallback(event);
598
635
  return;
599
636
  }
637
+ if (event.type === "irc_message") {
638
+ this.publishCustomMessage(event.message as OmpCustomDisplayMessage);
639
+ return;
640
+ }
600
641
  if (event.type === "notice") {
601
642
  this.noticeSequence += 1;
602
643
  this.publish({
@@ -836,8 +877,9 @@ export class OmpTimelineProjector {
836
877
 
837
878
  projectReplayMessage(message: OmpMessage): void {
838
879
  if (this.closed) return;
839
- const nativeIdentity = assistantIdentity(message);
840
880
  this.replaySequence += 1;
881
+ if (OMP_MESSAGE_REPLAY_POLICIES[message.role] === "ignore") return;
882
+
841
883
  if (message.role === "user") {
842
884
  this.replayToolCalls.clear();
843
885
  if (this.replayTurnId) this.finishTurn(this.replayTurnId);
@@ -858,7 +900,9 @@ export class OmpTimelineProjector {
858
900
  }
859
901
  return;
860
902
  }
903
+
861
904
  if (message.role === "assistant") {
905
+ const nativeIdentity = assistantIdentity(message);
862
906
  this.replayTurnId ??= `omp:replay-turn:${this.replaySequence}`;
863
907
  this.projectingReplay = true;
864
908
  try {
@@ -877,6 +921,7 @@ export class OmpTimelineProjector {
877
921
  if (nativeIdentity) this.rememberReplayOccurrence(nativeIdentity, message);
878
922
  return;
879
923
  }
924
+
880
925
  this.replayTurnId ??= `omp:replay-turn:${this.replaySequence}`;
881
926
  if (message.role === "toolResult") {
882
927
  const replayCall = this.replayToolCalls.get(message.toolCallId);
@@ -907,13 +952,18 @@ export class OmpTimelineProjector {
907
952
  );
908
953
  return;
909
954
  }
910
- if (message.role === "bashExecution") {
911
- const text = message.output
912
- ? `$ ${message.command}\n${message.output}`
913
- : `$ ${message.command}`;
914
- this.project({ type: "command_output", text }, this.replayTurnId);
915
- this.finishTurn(this.replayTurnId);
916
- this.replayTurnId = null;
955
+
956
+ if (
957
+ message.role === "bashExecution" ||
958
+ message.role === "pythonExecution" ||
959
+ message.role === "custom" ||
960
+ message.role === "hookMessage"
961
+ ) {
962
+ this.publishCustomMessage(message);
963
+ if (message.role === "bashExecution" || message.role === "pythonExecution") {
964
+ this.finishTurn(this.replayTurnId);
965
+ this.replayTurnId = null;
966
+ }
917
967
  }
918
968
  }
919
969
 
@@ -1007,13 +1057,28 @@ export class OmpTimelineProjector {
1007
1057
  if (!this.stream || this.closed || this.stream.dirtyBlocks.size === 0) return;
1008
1058
  const stream = this.stream;
1009
1059
  if (!stream.nativeIdentity && !finalizeFallback) return;
1010
- const indexes = [...stream.dirtyBlocks].sort((left, right) => left - right);
1060
+ const indexes = [...stream.blocks.keys()].sort((left, right) => left - right);
1061
+ let remainingTextBytes = MAX_STREAM_TEXT_LENGTH;
1011
1062
  stream.dirtyBlocks.clear();
1012
1063
  for (const contentIndex of indexes) {
1013
1064
  const block = stream.blocks.get(contentIndex);
1014
- if (!block?.text) continue;
1015
- const publicText = block.kind === "image" ? block.text : this.dataFilter.text(block.text);
1016
- if (!publicText || block.publishedText === publicText) continue;
1065
+ if (!block) continue;
1066
+ const publicText =
1067
+ block.kind === "image"
1068
+ ? block.text
1069
+ : truncateUtf8(
1070
+ this.dataFilter.text(block.text, MAX_REDACTED_STREAM_TEXT_LENGTH),
1071
+ remainingTextBytes,
1072
+ );
1073
+ if (block.kind !== "image") {
1074
+ remainingTextBytes = Math.max(0, remainingTextBytes - utf8Bytes(publicText));
1075
+ }
1076
+ if (
1077
+ block.publishedText === publicText ||
1078
+ (!publicText && block.publishedText === undefined)
1079
+ ) {
1080
+ continue;
1081
+ }
1017
1082
  const nextPublishedBytes = utf8Bytes(publicText);
1018
1083
  if (
1019
1084
  stream.retainedBytes + stream.publishedBytes + nextPublishedBytes >
@@ -1267,7 +1332,7 @@ export class OmpTimelineProjector {
1267
1332
  (snapshot.kind === "image" ? 0 : snapshotBytes);
1268
1333
  if (
1269
1334
  retainedBytes + stream.publishedBytes > MAX_STREAM_TOTAL_BYTES ||
1270
- textBytes > MAX_STREAM_TEXT_LENGTH
1335
+ textBytes > MAX_RAW_STREAM_TEXT_LENGTH
1271
1336
  ) {
1272
1337
  return;
1273
1338
  }
@@ -1291,7 +1356,7 @@ export class OmpTimelineProjector {
1291
1356
 
1292
1357
  private publishCommand(turnId: string): void {
1293
1358
  if (!this.commandText) return;
1294
- const publicText = this.dataFilter.text(this.commandText);
1359
+ const publicText = this.dataFilter.text(this.commandText, MAX_STREAM_TEXT_LENGTH);
1295
1360
  if (!publicText || publicText === this.commandPublishedText) return;
1296
1361
  this.commandPublishedText = publicText;
1297
1362
  this.publish({
@@ -1335,9 +1400,17 @@ export class OmpTimelineProjector {
1335
1400
  return undefined;
1336
1401
  }
1337
1402
 
1338
- private publishCustomMessage(message: OmpMessage): void {
1339
- if (message.display === false) return;
1340
- const rawType = message.customType ?? message.role;
1403
+ private publishCustomMessage(message: OmpCustomDisplayMessage): void {
1404
+ if (
1405
+ (message.role === "custom" || message.role === "hookMessage") &&
1406
+ message.display === false
1407
+ ) {
1408
+ return;
1409
+ }
1410
+ const rawType =
1411
+ message.role === "custom" || message.role === "hookMessage"
1412
+ ? (message.customType ?? message.role)
1413
+ : message.role;
1341
1414
  const publicType = this.dataFilter.text(rawType, 256);
1342
1415
  const lowerType = rawType.toLowerCase();
1343
1416
  const details = jsonRecord(this.dataFilter.json(message.details ?? null));
@@ -1346,7 +1419,9 @@ export class OmpTimelineProjector {
1346
1419
  const id = nativeIdentity
1347
1420
  ? `omp:custom:${createHash("sha256").update(nativeIdentity).digest("base64url").slice(0, 12)}`
1348
1421
  : `omp:custom:${this.customSequence}`;
1349
- const contentParts = Array.isArray(message.content) ? message.content : [];
1422
+ const messageContent =
1423
+ message.role === "custom" || message.role === "hookMessage" ? message.content : undefined;
1424
+ const contentParts = Array.isArray(messageContent) ? messageContent : [];
1350
1425
  const imageResult = nativeImageResult(
1351
1426
  {
1352
1427
  content:
@@ -1358,16 +1433,26 @@ export class OmpTimelineProjector {
1358
1433
  this.dataFilter,
1359
1434
  );
1360
1435
  const content =
1361
- typeof message.content === "string"
1362
- ? message.content
1436
+ typeof messageContent === "string"
1437
+ ? messageContent
1363
1438
  : contentParts
1364
1439
  .flatMap((part) => (part.type === "text" && part.text ? [part.text] : []))
1365
1440
  .join("\n\n");
1366
- if (message.role === "bashExecution" || /bash|shell|python/u.test(lowerType)) {
1441
+ const execution = message.role === "bashExecution" || message.role === "pythonExecution";
1442
+ if (execution || /bash|shell|python/u.test(lowerType)) {
1443
+ const nativeCommand =
1444
+ message.role === "bashExecution"
1445
+ ? message.command
1446
+ : message.role === "pythonExecution"
1447
+ ? message.code
1448
+ : undefined;
1367
1449
  const command = this.dataFilter.text(
1368
- message.command ?? firstString(details, "command", "input") ?? publicType,
1450
+ nativeCommand ?? firstString(details, "command", "input") ?? publicType,
1369
1451
  );
1370
- const output = message.output ?? content;
1452
+ const output = execution ? (message.output ?? content) : content;
1453
+ const exitCode = execution ? message.exitCode : undefined;
1454
+ const cancelled = execution ? message.cancelled : undefined;
1455
+ const truncated = execution ? message.truncated : undefined;
1371
1456
  this.publish({
1372
1457
  type: "tool_call",
1373
1458
  id,
@@ -1377,14 +1462,22 @@ export class OmpTimelineProjector {
1377
1462
  type: "shell",
1378
1463
  command,
1379
1464
  ...(firstString(details, "cwd") ? { cwd: firstString(details, "cwd") } : {}),
1380
- ...(output ? { output: this.dataFilter.text(output) } : {}),
1381
- ...(typeof message.exitCode === "number" || message.exitCode === null
1382
- ? { exitCode: message.exitCode }
1465
+ ...(output ? { output: this.dataFilter.text(output, MAX_STREAM_TEXT_LENGTH) } : {}),
1466
+ ...(typeof exitCode === "number" || exitCode === null
1467
+ ? { exitCode }
1383
1468
  : typeof details?.exitCode === "number"
1384
1469
  ? { exitCode: details.exitCode }
1385
1470
  : {}),
1386
1471
  },
1387
- status: message.cancelled ? "canceled" : "completed",
1472
+ ...(cancelled !== undefined || truncated !== undefined
1473
+ ? {
1474
+ metadata: {
1475
+ ...(cancelled !== undefined ? { cancelled } : {}),
1476
+ ...(truncated !== undefined ? { truncated } : {}),
1477
+ },
1478
+ }
1479
+ : {}),
1480
+ status: cancelled ? "canceled" : "completed",
1388
1481
  error: null,
1389
1482
  });
1390
1483
  if (imageResult) this.publishImages(id, publicType, imageResult.image);
@@ -0,0 +1,284 @@
1
+ import { arch, platform } from "node:os";
2
+ import type { RpcInput } from "@getpaseo/plugin";
3
+ import { z } from "zod";
4
+ import type { OmpProviderHealth, OmpVersion } from "../shared/provider-diagnostics";
5
+ import {
6
+ type getOmpSupportReport,
7
+ OMP_SUPPORT_REPORT_MAX_BYTES,
8
+ OMP_SUPPORT_REPORT_SCHEMA_VERSION,
9
+ supportReportByteLength,
10
+ } from "../shared/support-diagnostics";
11
+ import type {
12
+ OmpOperationalFailureCollector,
13
+ OmpOperationalFailureSummary,
14
+ } from "./operational-failure-diagnostics";
15
+ import { PASEO_OMP_PACKAGE_VERSION } from "./package-version";
16
+ import type {
17
+ OmpProtocolViolationCollector,
18
+ OmpProtocolViolationSummary,
19
+ } from "./protocol-violation-diagnostics";
20
+ import { resolveGetOmpProviderHealth } from "./provider-diagnostics";
21
+
22
+ const PACKAGE_VERSION = z
23
+ .string()
24
+ .regex(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]{1,48})?$/u)
25
+ .max(64);
26
+ const NODE_VERSION = z
27
+ .string()
28
+ .regex(/^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]{1,48})?$/u)
29
+ .max(64);
30
+ const KNOWN_PLATFORMS: Record<string, true> = {
31
+ aix: true,
32
+ android: true,
33
+ darwin: true,
34
+ freebsd: true,
35
+ linux: true,
36
+ openbsd: true,
37
+ sunos: true,
38
+ win32: true,
39
+ };
40
+ const KNOWN_ARCHITECTURES: Record<string, true> = {
41
+ arm: true,
42
+ arm64: true,
43
+ ia32: true,
44
+ loong64: true,
45
+ mips: true,
46
+ mipsel: true,
47
+ ppc: true,
48
+ ppc64: true,
49
+ riscv64: true,
50
+ s390: true,
51
+ s390x: true,
52
+ x64: true,
53
+ };
54
+
55
+ export interface OmpSupportReportData {
56
+ collectedAt: string;
57
+ pluginVersion: string;
58
+ platform: string;
59
+ architecture: string;
60
+ nodeVersion: string;
61
+ scope: "global" | "workspace";
62
+ store: "default" | "named-profile" | "custom-directory";
63
+ health: OmpProviderHealth | null;
64
+ violations: readonly OmpProtocolViolationSummary[];
65
+ operationalFailures: readonly OmpOperationalFailureSummary[];
66
+ }
67
+
68
+ const EXPECTATION_LABELS: Record<
69
+ NonNullable<OmpProtocolViolationSummary["latestExpected"]>,
70
+ string
71
+ > = {
72
+ "complete-json-line": "newline-terminated JSON frame",
73
+ "within-byte-limit": "byte length within negotiated limit",
74
+ "valid-json": "valid UTF-8 JSON",
75
+ "object-envelope": "JSON object envelope",
76
+ "bounded-frame-type": "frame type string up to 64 bytes",
77
+ "valid-response-frame": "valid response envelope",
78
+ "valid-ready-frame": "valid ready handshake",
79
+ "single-ready-frame": "one ready handshake",
80
+ "valid-event-frame": "valid event payload",
81
+ "valid-event-state-transition": "event valid for current stream state",
82
+ "valid-message-event": "valid message event payload",
83
+ "valid-tool-event": "valid tool event payload",
84
+ "valid-lifecycle-event": "valid lifecycle event payload",
85
+ "valid-subagent-event": "valid subagent event payload",
86
+ "valid-configuration-event": "valid configuration event payload",
87
+ "valid-extension-ui-event": "valid extension UI event payload",
88
+ "known-event-type": "supported event type",
89
+ "notice-level-enum": "info, warning, or error",
90
+ "notice-message-string": "bounded string",
91
+ "valid-chunk-frame": "valid chunk metadata",
92
+ "valid-base64-chunk": "valid base64 chunk",
93
+ "first-chunk-index-zero": "first chunk index 0",
94
+ "contiguous-chunk-sequence": "contiguous chunk sequence",
95
+ "declared-chunk-byte-count": "decoded bytes equal declared byte length",
96
+ "chunk-before-deadline": "next chunk before timeout",
97
+ "no-interleaved-frame": "no interleaved frame during chunk assembly",
98
+ "no-remote-frame-error": "no remote frame error",
99
+ };
100
+
101
+ function finiteCount(value: number | null | undefined, maximum = Number.MAX_SAFE_INTEGER): string {
102
+ return value !== null && value !== undefined && Number.isSafeInteger(value) && value >= 0
103
+ ? String(Math.min(value, maximum))
104
+ : "unknown";
105
+ }
106
+
107
+ function usefulReportLine(line: string): boolean {
108
+ return !line.endsWith(": unavailable") && !line.endsWith(": unknown") && !line.endsWith(": 0");
109
+ }
110
+
111
+ function formatVersion(version: OmpVersion | null): string {
112
+ if (!version) return "unavailable";
113
+ const core = `${version.major}.${version.minor}.${version.patch}`;
114
+ return version.prerelease ? `${core}-${version.prerelease}` : core;
115
+ }
116
+
117
+ function formatHealth(health: OmpProviderHealth | null): string[] {
118
+ if (!health) return ["collection.provider_health: failed"];
119
+ return [
120
+ "collection.provider_health: complete",
121
+ `omp.installed: ${health.binary.installed}`,
122
+ `omp.version: ${formatVersion(health.binary.version)}`,
123
+ `omp.version_probe: ${health.binary.versionStatus}`,
124
+ `omp.process_cleanup: ${health.binary.processCleanupFailed ? "failed" : "ok"}`,
125
+ `compatibility.rpc_ui: ${!health.rpcUi.checked || health.rpcUi.supported === null ? "unknown" : health.rpcUi.supported ? "supported" : "not-advertised"}`,
126
+ `compatibility.lsp: ${health.lsp.status}`,
127
+ `mcp.status: ${health.mcp.status}`,
128
+ `mcp.server_count: ${finiteCount(health.mcp.serverCount, 4_096)}`,
129
+ `mcp.reason: ${health.mcp.reason ?? "unknown"}`,
130
+ `storage.agent_root: ${health.roots.agentRootState}`,
131
+ `storage.config: ${health.roots.configState}`,
132
+ `storage.session_root: ${health.roots.sessionRootState}`,
133
+ `storage.agent_database: ${health.databases.agentDbState}`,
134
+ `storage.history_database: ${health.databases.historyDbState}`,
135
+ `storage.memory_backend: ${health.memoryBackend ?? "unknown"}`,
136
+ `hub.status: ${health.process.status}`,
137
+ `hub.tracked_count: ${finiteCount(health.process.trackedCount, 100_000)}`,
138
+ `hub.active_count: ${finiteCount(health.process.activeCount, 100_000)}`,
139
+ `hub.historical_count: ${finiteCount(health.process.historicalCount, 100_000)}`,
140
+ `hub.unknown_count: ${finiteCount(health.process.unknownCount, 100_000)}`,
141
+ ];
142
+ }
143
+
144
+ /** Stable line ordering is part of the pasted support-report contract. */
145
+ export function formatOmpSupportReport(data: OmpSupportReportData): string {
146
+ const lines = [
147
+ "OMP support diagnostics",
148
+ `schema_version: ${OMP_SUPPORT_REPORT_SCHEMA_VERSION}`,
149
+ `collected_at_utc: ${data.collectedAt}`,
150
+ `paseo_omp.version: ${data.pluginVersion}`,
151
+ `runtime.platform: ${data.platform}`,
152
+ `runtime.architecture: ${data.architecture}`,
153
+ `runtime.node: ${data.nodeVersion}`,
154
+ `selection.scope: ${data.scope}`,
155
+ `selection.store: ${data.store}`,
156
+ "selection.applies_to: provider-health",
157
+ "diagnostic_counters.scope: plugin-process-all-stores-workspaces",
158
+ "diagnostic_counters.lifetime: since-plugin-load",
159
+ ...formatHealth(data.health),
160
+ ];
161
+ for (const violation of data.violations) {
162
+ if (violation.occurrenceCount === 0) continue;
163
+ const prefix = `protocol.${violation.category}`;
164
+ lines.push(
165
+ `${prefix}.occurrence_count: ${finiteCount(violation.occurrenceCount)}`,
166
+ `${prefix}.batch_count: ${finiteCount(violation.batchCount)}`,
167
+ `${prefix}.max_batch_count: ${finiteCount(violation.maxOccurrenceCount)}`,
168
+ );
169
+ for (const [reason, count] of Object.entries(violation.reasonCounts)) {
170
+ if (count > 0) lines.push(`${prefix}.reason.${reason}.occurrence_count: ${count}`);
171
+ }
172
+ lines.push(
173
+ `${prefix}.latest_reason: ${violation.latestReason ?? "unknown"}`,
174
+ `${prefix}.latest_phase: ${violation.latestPhase ?? "unknown"}`,
175
+ `${prefix}.latest_event_type: ${violation.latestEventType ?? "unknown"}`,
176
+ `${prefix}.first_at_utc: ${violation.firstAt ?? "unavailable"}`,
177
+ `${prefix}.last_at_utc: ${violation.lastAt ?? "unavailable"}`,
178
+ `${prefix}.latest_frame_type: ${violation.latestFrameType ?? "unknown"}`,
179
+ `${prefix}.latest_field: ${violation.latestField ?? "unknown"}`,
180
+ `${prefix}.latest_expected: ${violation.latestExpected ? EXPECTATION_LABELS[violation.latestExpected] : "unknown"}`,
181
+ `${prefix}.latest_actual_type: ${violation.latestActualType ?? "unknown"}`,
182
+ `${prefix}.max_byte_size: ${finiteCount(violation.maxByteSize)}`,
183
+ `${prefix}.latest_limit_bytes: ${finiteCount(violation.latestLimitBytes)}`,
184
+ );
185
+ }
186
+ for (const failure of data.operationalFailures) {
187
+ if (failure.occurrenceCount === 0) continue;
188
+ const prefix = `operational.${failure.category}.${failure.stage}`;
189
+ lines.push(
190
+ `${prefix}.occurrence_count: ${finiteCount(failure.occurrenceCount)}`,
191
+ `${prefix}.first_at_utc: ${failure.firstAt ?? "unavailable"}`,
192
+ `${prefix}.last_at_utc: ${failure.lastAt ?? "unavailable"}`,
193
+ );
194
+ }
195
+ const report = `${lines.filter(usefulReportLine).join("\n")}\n`;
196
+ if (supportReportByteLength(report) > OMP_SUPPORT_REPORT_MAX_BYTES) {
197
+ return [
198
+ "OMP support diagnostics",
199
+ `schema_version: ${OMP_SUPPORT_REPORT_SCHEMA_VERSION}`,
200
+ `collected_at_utc: ${data.collectedAt}`,
201
+ "collection_status: failed",
202
+ "collection_error: report-size-limit",
203
+ "",
204
+ ].join("\n");
205
+ }
206
+ return report;
207
+ }
208
+
209
+ function collectedAt(now: () => Date): string {
210
+ try {
211
+ const value = now();
212
+ return Number.isFinite(value.getTime()) ? value.toISOString() : "unavailable";
213
+ } catch {
214
+ return "unavailable";
215
+ }
216
+ }
217
+
218
+ export async function resolveGetOmpSupportReport(
219
+ input: RpcInput<typeof getOmpSupportReport>,
220
+ violations: OmpProtocolViolationCollector,
221
+ operationalFailures: OmpOperationalFailureCollector,
222
+ dependencies: {
223
+ loadHealth?: typeof resolveGetOmpProviderHealth;
224
+ loadPluginVersion?: () => Promise<string>;
225
+ now?: () => Date;
226
+ platform?: () => string;
227
+ architecture?: () => string;
228
+ nodeVersion?: string;
229
+ } = {},
230
+ ): Promise<{ report: string }> {
231
+ const timestamp = collectedAt(dependencies.now ?? (() => new Date()));
232
+ try {
233
+ const [healthResult, pluginVersionResult] = await Promise.allSettled([
234
+ (dependencies.loadHealth ?? resolveGetOmpProviderHealth)(input),
235
+ (dependencies.loadPluginVersion ?? (() => Promise.resolve(PASEO_OMP_PACKAGE_VERSION)))(),
236
+ ]);
237
+ const platformValue = (dependencies.platform ?? platform)();
238
+ const architectureValue = (dependencies.architecture ?? arch)();
239
+ const nodeVersionValue = dependencies.nodeVersion ?? process.version;
240
+ return {
241
+ report: formatOmpSupportReport({
242
+ collectedAt: timestamp,
243
+ pluginVersion:
244
+ pluginVersionResult.status === "fulfilled" &&
245
+ PACKAGE_VERSION.safeParse(pluginVersionResult.value).success
246
+ ? pluginVersionResult.value
247
+ : "unavailable",
248
+ platform: KNOWN_PLATFORMS[platformValue] ? platformValue : "unknown",
249
+ architecture: KNOWN_ARCHITECTURES[architectureValue] ? architectureValue : "unknown",
250
+ nodeVersion: NODE_VERSION.safeParse(nodeVersionValue).success
251
+ ? nodeVersionValue
252
+ : "unavailable",
253
+ scope: input.cwd ? "workspace" : "global",
254
+ store: input.store?.profile
255
+ ? "named-profile"
256
+ : input.store?.agentDir
257
+ ? "custom-directory"
258
+ : "default",
259
+ health: healthResult.status === "fulfilled" ? healthResult.value : null,
260
+ violations: violations.snapshot(),
261
+ operationalFailures: operationalFailures.snapshot(),
262
+ }),
263
+ };
264
+ } catch {
265
+ return {
266
+ report: formatOmpSupportReport({
267
+ collectedAt: timestamp,
268
+ pluginVersion: "unavailable",
269
+ platform: "unknown",
270
+ architecture: "unknown",
271
+ nodeVersion: "unavailable",
272
+ scope: input.cwd ? "workspace" : "global",
273
+ store: input.store?.profile
274
+ ? "named-profile"
275
+ : input.store?.agentDir
276
+ ? "custom-directory"
277
+ : "default",
278
+ health: null,
279
+ violations: violations.snapshot(),
280
+ operationalFailures: operationalFailures.snapshot(),
281
+ }),
282
+ };
283
+ }
284
+ }
@@ -0,0 +1,49 @@
1
+ import { defineRpc } from "@getpaseo/plugin";
2
+ import { z } from "zod";
3
+ import { OmpWorkspaceCwdSchema } from "./hub";
4
+ import { OmpStoreSchema } from "./omp-store";
5
+
6
+ const OMP_MODEL_TEXT_LIMIT = 256;
7
+ const OMP_MODEL_SELECTOR_LIMIT = OMP_MODEL_TEXT_LIMIT * 2 + 1;
8
+ const OMP_MODEL_LIST_LIMIT = 256;
9
+ const OMP_MODEL_INPUT_LIMIT = 16;
10
+ const OMP_THINKING_LEVEL_LIMIT = 16;
11
+ const OMP_THINKING_LEVEL_TEXT_LIMIT = 32;
12
+ const OMP_CONTEXT_WINDOW_LIMIT = 100_000_000;
13
+
14
+ const OmpModelTextSchema = z.string().min(1).max(OMP_MODEL_TEXT_LIMIT);
15
+
16
+ export const OmpModelCandidateSchema = z
17
+ .object({
18
+ selector: z.string().min(3).max(OMP_MODEL_SELECTOR_LIMIT),
19
+ provider: OmpModelTextSchema,
20
+ id: OmpModelTextSchema,
21
+ name: z.string().max(OMP_MODEL_TEXT_LIMIT).optional(),
22
+ reasoning: z.boolean(),
23
+ input: z.array(OmpModelTextSchema).max(OMP_MODEL_INPUT_LIMIT),
24
+ contextWindow: z
25
+ .number()
26
+ .int()
27
+ .nonnegative()
28
+ .max(OMP_CONTEXT_WINDOW_LIMIT)
29
+ .nullable()
30
+ .optional(),
31
+ thinkingLevels: z
32
+ .array(z.string().max(OMP_THINKING_LEVEL_TEXT_LIMIT))
33
+ .max(OMP_THINKING_LEVEL_LIMIT),
34
+ })
35
+ .strict();
36
+ export type OmpModelCandidate = z.infer<typeof OmpModelCandidateSchema>;
37
+
38
+ export const OmpModelListResultSchema = z
39
+ .object({ models: z.array(OmpModelCandidateSchema).max(OMP_MODEL_LIST_LIMIT) })
40
+ .strict();
41
+ export type OmpModelListResult = z.infer<typeof OmpModelListResultSchema>;
42
+
43
+ export const listOmpModels = defineRpc({
44
+ name: "paseo-omp.list-models",
45
+ input: z
46
+ .object({ store: OmpStoreSchema.optional(), cwd: OmpWorkspaceCwdSchema.optional() })
47
+ .strict(),
48
+ output: OmpModelListResultSchema,
49
+ });