@omercnet/paseo-omp 0.3.0-next.108.1 → 0.3.0-next.111.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.
@@ -43,7 +43,7 @@ function profileDirectory(environment: NodeJS.ProcessEnv): string {
43
43
  );
44
44
  }
45
45
 
46
- /** Paseo 0.8 contribution registration is synchronous; inspect directory names only. */
46
+ /** Contribution registration is synchronous; inspect directory names only. */
47
47
  export function discoverOmpProfilesSync(environment: NodeJS.ProcessEnv = process.env): string[] {
48
48
  let directory: Dir;
49
49
  try {
@@ -151,7 +151,12 @@ export function createProfileOmpProvider(profile: string, options: OmpProviderOp
151
151
  const id = profileProviderId(profile);
152
152
  const environment = fixedEnvironment(profile, options.environment ?? process.env);
153
153
  const sessionDir = resolve(ompSessionDir(environment));
154
- const runtime = options.runtime ?? new OmpRpcRuntime({ environment });
154
+ const runtime =
155
+ options.runtime ??
156
+ new OmpRpcRuntime({
157
+ environment,
158
+ reportProtocolViolation: options.reportProtocolViolation,
159
+ });
155
160
  const readPersistedSessionTranscript = runtime.readPersistedSessionTranscript?.bind(runtime);
156
161
  const assertSessionDir = (requested?: string) => {
157
162
  if (requested !== undefined && resolve(requested) !== sessionDir) {
@@ -3,10 +3,11 @@ import { homedir } from "node:os";
3
3
  import type { ProviderRegistration } from "@getpaseo/plugin/server/provider";
4
4
  import { z } from "zod";
5
5
  import type { OmpBrowserAuthorizationRegistry } from "../mcp-browser";
6
+ import type { OmpOperationalFailureReporter } from "../operational-failure-diagnostics";
6
7
  import { probeOmpAvailability } from "../provider-diagnostics";
7
8
  import { createOmpConnection, OmpNativeSessionReservations } from "./connection";
8
9
  import type { OmpMcpConnector } from "./host-tools";
9
- import { OmpRpcRuntime, type OmpRuntime } from "./omp-rpc";
10
+ import { type OmpProtocolViolationDiagnostic, OmpRpcRuntime, type OmpRuntime } from "./omp-rpc";
10
11
  import { parseOmpProviderOptions } from "./provider-options";
11
12
  import { boundedJsonBytes } from "./security";
12
13
  import { OmpProviderOptionsSchema } from "./settings";
@@ -66,6 +67,8 @@ export interface OmpProviderOptions {
66
67
  mcpInitializationTimeoutMs?: number;
67
68
  mcpConnector?: OmpMcpConnector;
68
69
  browserAuthorizationRegistry?: OmpBrowserAuthorizationRegistry;
70
+ reportProtocolViolation?: (diagnostic: OmpProtocolViolationDiagnostic) => void | Promise<void>;
71
+ reportOperationalFailure?: OmpOperationalFailureReporter;
69
72
  availabilityProbe?: (
70
73
  options: ProviderCatalogOptionsCompat,
71
74
  timeoutMs: number | undefined,
@@ -85,7 +88,12 @@ function stableJson(value: unknown): string {
85
88
  }
86
89
 
87
90
  export function createOmpProvider(options: OmpProviderOptions = {}): ProviderRegistrationCompat {
88
- const runtime = options.runtime ?? new OmpRpcRuntime({ environment: options.environment });
91
+ const runtime =
92
+ options.runtime ??
93
+ new OmpRpcRuntime({
94
+ environment: options.environment,
95
+ reportProtocolViolation: options.reportProtocolViolation,
96
+ });
89
97
  const nativeReservations = new OmpNativeSessionReservations();
90
98
  return {
91
99
  id: "omp-plugin",
@@ -156,6 +164,8 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
156
164
  options.mcpInitializationTimeoutMs,
157
165
  options.replayTimeoutMs,
158
166
  options.browserAuthorizationRegistry,
167
+ undefined,
168
+ options.reportOperationalFailure,
159
169
  );
160
170
  },
161
171
  };
@@ -11,6 +11,10 @@ import type {
11
11
  ProviderUsage,
12
12
  } from "@getpaseo/plugin/server/provider";
13
13
  import { getForgeDefinitionOrNeutral } from "@getpaseo/protocol/forge-manifest";
14
+ import type {
15
+ OmpOperationalFailure,
16
+ OmpOperationalFailureReporter,
17
+ } from "../operational-failure-diagnostics";
14
18
  import {
15
19
  mapOmpModels,
16
20
  nativeOmpModelId,
@@ -292,6 +296,7 @@ type ActiveTurn = {
292
296
  terminalizing: boolean;
293
297
  terminalization?: Promise<void>;
294
298
  terminalOutcome?: TurnOutcome;
299
+ operationalTerminalStage?: "failed" | "unresolved";
295
300
  terminalWake?: VoidDeferred;
296
301
  steerReady: VoidDeferred;
297
302
  steersInFlight: number;
@@ -973,6 +978,7 @@ export class OmpProviderSession {
973
978
  private readonly transitionNativeSession: NativeSessionTransition,
974
979
  private readonly quarantineRewindCleanup: RewindCleanupQuarantine,
975
980
  private readonly retireRewindSession: RewindSessionRetirement,
981
+ private readonly reportOperationalFailure: OmpOperationalFailureReporter,
976
982
  scheduler: OmpTimelineScheduler = defaultOmpTimelineScheduler,
977
983
  ) {
978
984
  this.id = id;
@@ -1006,6 +1012,13 @@ export class OmpProviderSession {
1006
1012
  : null;
1007
1013
  this.bindRuntime(runtime);
1008
1014
  }
1015
+ private recordOperationalFailure(failure: OmpOperationalFailure): void {
1016
+ try {
1017
+ this.reportOperationalFailure(failure);
1018
+ } catch {
1019
+ // Diagnostics must never affect provider or session flow.
1020
+ }
1021
+ }
1009
1022
  get persistenceSessionId(): string | undefined {
1010
1023
  return this.persistSession ? this.nativeSessionId : undefined;
1011
1024
  }
@@ -1032,6 +1045,7 @@ export class OmpProviderSession {
1032
1045
  environment?: NodeJS.ProcessEnv,
1033
1046
  mcpConnector?: OmpMcpConnector,
1034
1047
  mcpInitializationTimeoutMs?: number,
1048
+ reportOperationalFailure: OmpOperationalFailureReporter = () => {},
1035
1049
  ): Promise<OmpProviderSession> {
1036
1050
  const resumeSessionId = ompPersistenceSessionId(input);
1037
1051
  if (resumeSessionId && !input.config.persist) {
@@ -1078,6 +1092,7 @@ export class OmpProviderSession {
1078
1092
  connectMcp: mcpConnector,
1079
1093
  signal,
1080
1094
  initializationTimeoutMs: mcpInitializationTimeoutMs,
1095
+ reportOperationalFailure,
1081
1096
  });
1082
1097
  let native: OmpRuntimeSession | undefined;
1083
1098
  let cleanupNativeSessionId: string | undefined;
@@ -1236,6 +1251,7 @@ export class OmpProviderSession {
1236
1251
  transitionNativeSession,
1237
1252
  quarantineRewindCleanup,
1238
1253
  retireRewindSession,
1254
+ reportOperationalFailure,
1239
1255
  scheduler,
1240
1256
  );
1241
1257
  session.unsupportedThinkingNoticePending = unsupportedThinkingLevel;
@@ -1653,6 +1669,10 @@ export class OmpProviderSession {
1653
1669
  }
1654
1670
  } catch (error) {
1655
1671
  if (replay.signal.aborted) throw error;
1672
+ this.recordOperationalFailure({
1673
+ category: "replay-recovery",
1674
+ stage: "persisted-replay",
1675
+ });
1656
1676
  this.emit({
1657
1677
  type: "timeline.item",
1658
1678
  sessionId: this.id,
@@ -1807,6 +1827,7 @@ export class OmpProviderSession {
1807
1827
  this.publishCommittedConfig(state, runtime, generation);
1808
1828
  this.emit({ type: "request.completed", requestId: input.requestId });
1809
1829
  } catch (error) {
1830
+ this.recordOperationalFailure({ category: "replay-recovery", stage: "rewind" });
1810
1831
  const failure = branchMutationPossible
1811
1832
  ? { message: "OMP conversation rewind left native state indeterminate" }
1812
1833
  : providerError(error, "OMP conversation rewind failed");
@@ -2924,7 +2945,13 @@ export class OmpProviderSession {
2924
2945
 
2925
2946
  private async recoverRuntime(): Promise<void> {
2926
2947
  if (!this.runtimeDead) return;
2927
- this.recoveryPromise ??= this.startRecovery();
2948
+ this.recoveryPromise ??= this.startRecovery().catch((error) => {
2949
+ this.recordOperationalFailure({
2950
+ category: "replay-recovery",
2951
+ stage: "runtime-recovery",
2952
+ });
2953
+ throw error;
2954
+ });
2928
2955
  try {
2929
2956
  await this.recoveryPromise;
2930
2957
  } finally {
@@ -3147,6 +3174,10 @@ export class OmpProviderSession {
3147
3174
  try {
3148
3175
  this.subsessions?.handle(event);
3149
3176
  } catch {
3177
+ this.recordOperationalFailure({
3178
+ category: "tool-projector",
3179
+ stage: "subsession-projector",
3180
+ });
3150
3181
  this.handleRuntimeFailure("OMP subagent event processing failed");
3151
3182
  }
3152
3183
  return;
@@ -3381,6 +3412,10 @@ export class OmpProviderSession {
3381
3412
  try {
3382
3413
  this.subsessions?.observeSessionEvent(this.id, event);
3383
3414
  } catch {
3415
+ this.recordOperationalFailure({
3416
+ category: "tool-projector",
3417
+ stage: "subsession-projector",
3418
+ });
3384
3419
  this.handleRuntimeFailure("OMP subagent dispatch tracking failed");
3385
3420
  return;
3386
3421
  }
@@ -3437,7 +3472,15 @@ export class OmpProviderSession {
3437
3472
  }
3438
3473
  }
3439
3474
  if (isNativeTurnActivity(event)) this.markAgentEvidence(turn);
3440
- this.projector.project(event, turn.turnId);
3475
+ try {
3476
+ this.projector.project(event, turn.turnId);
3477
+ } catch (error) {
3478
+ this.recordOperationalFailure({
3479
+ category: "tool-projector",
3480
+ stage: "timeline-projector",
3481
+ });
3482
+ throw error;
3483
+ }
3441
3484
  if (event.type === "tool_execution_end") this.resumeDeferredAgentEnd();
3442
3485
  }
3443
3486
 
@@ -4506,11 +4549,13 @@ export class OmpProviderSession {
4506
4549
  const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
4507
4550
  if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
4508
4551
  if (!state) {
4552
+ turn.operationalTerminalStage = "unresolved";
4509
4553
  this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
4510
4554
  return;
4511
4555
  }
4512
4556
  turn.deferredAgentEnd = undefined;
4513
4557
  if (state.isStreaming || state.isCompacting) return;
4558
+ turn.operationalTerminalStage = "unresolved";
4514
4559
  await this.finishTurn(turn, "failed", {
4515
4560
  message: "OMP unkeyed agent_end could not be correlated to the current prompt",
4516
4561
  });
@@ -4591,6 +4636,7 @@ export class OmpProviderSession {
4591
4636
  ) {
4592
4637
  void this.completeAgentEnd(turn, candidate.event);
4593
4638
  } else {
4639
+ turn.operationalTerminalStage = "unresolved";
4594
4640
  this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
4595
4641
  }
4596
4642
  }, AGENT_END_SETTLE_MS);
@@ -4615,6 +4661,10 @@ export class OmpProviderSession {
4615
4661
  turn.terminalizing = false;
4616
4662
  turn.deferredAgentEnd = candidate;
4617
4663
  void this.subsessions.reconcile(this.runtime).catch(() => {
4664
+ this.recordOperationalFailure({
4665
+ category: "tool-projector",
4666
+ stage: "subsession-projector",
4667
+ });
4618
4668
  if (!turn.terminal && this.activeTurn === turn) {
4619
4669
  this.subsessions?.terminalize("failed");
4620
4670
  this.resumeDeferredAgentEnd();
@@ -4700,6 +4750,7 @@ export class OmpProviderSession {
4700
4750
  return;
4701
4751
  }
4702
4752
  if (turn.userEchoObserved) {
4753
+ turn.operationalTerminalStage = "unresolved";
4703
4754
  this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
4704
4755
  return;
4705
4756
  }
@@ -4759,6 +4810,7 @@ export class OmpProviderSession {
4759
4810
  await this.finishTurn(turn, "completed");
4760
4811
  return;
4761
4812
  }
4813
+ turn.operationalTerminalStage = outcome === "failed" ? "failed" : "unresolved";
4762
4814
  const error =
4763
4815
  outcome === "failed" ? "OMP assistant turn failed" : unknownTerminalOutcomeError(event, turn);
4764
4816
  this.subsessions?.terminalize("failed");
@@ -4901,6 +4953,12 @@ export class OmpProviderSession {
4901
4953
  usageSampled: true,
4902
4954
  };
4903
4955
  }
4956
+ if (outcome.state === "failed") {
4957
+ this.recordOperationalFailure({
4958
+ category: "terminal-outcome",
4959
+ stage: turn.operationalTerminalStage ?? "failed",
4960
+ });
4961
+ }
4904
4962
  this.imageMaterializer.clear();
4905
4963
  turn.terminal = true;
4906
4964
  if (this.usageSample?.turn === turn) this.usageSample = null;
@@ -4914,7 +4972,15 @@ export class OmpProviderSession {
4914
4972
  }
4915
4973
  this.publishPendingUsers(turn);
4916
4974
  this.resolveTurnPermissions(turn.turnId);
4917
- this.projector.finishTurn(turn.turnId, preserveCompactions);
4975
+ try {
4976
+ this.projector.finishTurn(turn.turnId, preserveCompactions);
4977
+ } catch (error) {
4978
+ this.recordOperationalFailure({
4979
+ category: "tool-projector",
4980
+ stage: "timeline-projector",
4981
+ });
4982
+ throw error;
4983
+ }
4918
4984
  this.unclaimedBranchEntries.length = 0;
4919
4985
  this.emit({
4920
4986
  type: "session.turn",
@@ -4972,6 +5038,7 @@ export class OmpProviderSession {
4972
5038
  this.publishPromptResult(turn, { type: "failed", error: { message } });
4973
5039
  if (turn.started) void this.finishTurn(turn, "failed", { message }, true, true);
4974
5040
  else {
5041
+ this.recordOperationalFailure({ category: "terminal-outcome", stage: "failed" });
4975
5042
  turn.steerReady.resolve();
4976
5043
  turn.terminal = true;
4977
5044
  this.projector.finishTurn(turn.turnId);
@@ -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,32 @@
1
+ import { defineRpc } from "@getpaseo/plugin";
2
+ import { z } from "zod";
3
+ import { OmpWorkspaceCwdSchema } from "./hub";
4
+ import { OmpStoreSchema } from "./omp-store";
5
+
6
+ export const OMP_SUPPORT_REPORT_SCHEMA_VERSION = 1 as const;
7
+ export const OMP_SUPPORT_REPORT_MAX_BYTES = 64 * 1024;
8
+ export const OMP_SUPPORT_ISSUE_URL =
9
+ "https://github.com/omercnet/paseo-plugins/issues/new?template=omp-plugin.yml";
10
+
11
+ export function supportReportByteLength(value: string): number {
12
+ return new TextEncoder().encode(value).byteLength;
13
+ }
14
+
15
+ export const OmpSupportReportTextSchema = z
16
+ .string()
17
+ .min(1)
18
+ .refine((value) => supportReportByteLength(value) <= OMP_SUPPORT_REPORT_MAX_BYTES, {
19
+ message: "OMP support report exceeds 64 KiB",
20
+ });
21
+
22
+ export const getOmpSupportReport = defineRpc({
23
+ name: "paseo-omp.get-support-report",
24
+ input: z
25
+ .object({
26
+ store: OmpStoreSchema.optional(),
27
+ force: z.boolean().optional(),
28
+ cwd: OmpWorkspaceCwdSchema.optional(),
29
+ })
30
+ .strict(),
31
+ output: z.object({ report: OmpSupportReportTextSchema }).strict(),
32
+ });