@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.
- package/README.md +19 -5
- package/client/omp-config-surface.tsx +243 -24
- package/client/omp-config-views.ts +24 -0
- package/client/omp-model-picker-state.ts +145 -0
- package/client/omp-model-picker.tsx +282 -0
- package/client/omp-routing-editor.tsx +307 -0
- package/client/support-diagnostics-state.ts +45 -0
- package/index.server.ts +27 -2
- package/package.json +4 -8
- package/paseo-plugin.json +1 -1
- package/server/omp-models.ts +59 -0
- package/server/omp-settings.ts +30 -20
- package/server/operational-failure-diagnostics.ts +76 -0
- package/server/package-version.ts +2 -0
- package/server/protocol-violation-diagnostics.ts +169 -0
- package/server/provider/catalog.ts +39 -10
- package/server/provider/connection.ts +88 -9
- package/server/provider/host-tools.ts +284 -34
- package/server/provider/mcp-transport.ts +2 -1
- package/server/provider/omp-rpc.ts +1785 -267
- package/server/provider/profile-providers.ts +7 -2
- package/server/provider/registration.ts +12 -2
- package/server/provider/security.ts +8 -10
- package/server/provider/session-descriptors.ts +97 -29
- package/server/provider/session.ts +212 -59
- package/server/provider/subsessions.ts +334 -74
- package/server/provider/timeline-projector.ts +125 -32
- package/server/support-diagnostics.ts +284 -0
- package/shared/omp-models.ts +49 -0
- package/shared/omp-settings.ts +227 -3
- package/shared/support-diagnostics.ts +32 -0
- package/CHANGELOG.md +0 -113
- package/SUPPORT.md +0 -44
- package/TESTING.md +0 -150
- package/docs/alpha-release-checklist.md +0 -68
- package/docs/configuration.md +0 -126
- package/docs/core-provider-issue-audit.md +0 -109
- package/docs/images/mcp-authorization-compact.png +0 -0
- package/docs/images/mcp-controls-wide.png +0 -0
- package/docs/images/plugin-manager.png +0 -0
- package/docs/images/workspace-settings.png +0 -0
- package/docs/installation.md +0 -89
- package/tsconfig.json +0 -16
|
@@ -43,7 +43,7 @@ function profileDirectory(environment: NodeJS.ProcessEnv): string {
|
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
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 =
|
|
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 =
|
|
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
|
};
|
|
@@ -83,16 +83,14 @@ function truncateJsonString(value: string, maxBytes: number): string {
|
|
|
83
83
|
export function truncateUtf8(value: string, maxBytes: number): string {
|
|
84
84
|
if (utf8Bytes(value) <= maxBytes) return value;
|
|
85
85
|
const suffix = "<truncated>";
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
return `${output}${suffix}`;
|
|
86
|
+
const suffixBytes = utf8Bytes(suffix);
|
|
87
|
+
if (maxBytes <= 0) return "";
|
|
88
|
+
if (maxBytes < suffixBytes) return suffix.slice(0, Math.floor(maxBytes));
|
|
89
|
+
|
|
90
|
+
const bytes = Buffer.from(value, "utf8");
|
|
91
|
+
let end = Math.min(maxBytes - suffixBytes, bytes.byteLength);
|
|
92
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end -= 1;
|
|
93
|
+
return `${bytes.subarray(0, end).toString("utf8")}${suffix}`;
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
export interface BoundedJsonMetrics {
|
|
@@ -2,8 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { constants, type Dir } from "node:fs";
|
|
3
3
|
import { type FileHandle, lstat, open, opendir, realpath } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { basename,
|
|
5
|
+
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { ompSessionDir } from "../paths";
|
|
7
|
+
import { isValidImagePayload } from "./image";
|
|
7
8
|
|
|
8
9
|
const MAX_DESCRIPTOR_PREFIX_BYTES = 64 * 1024;
|
|
9
10
|
const MAX_DESCRIPTOR_SUFFIX_BYTES = 64 * 1024;
|
|
@@ -48,6 +49,7 @@ export interface OmpPersistedSessionTranscript {
|
|
|
48
49
|
nativeSessionId: string;
|
|
49
50
|
byteLength: number;
|
|
50
51
|
messages: unknown[];
|
|
52
|
+
imageReplayWarning?: true;
|
|
51
53
|
}
|
|
52
54
|
export interface OmpSessionListOptions {
|
|
53
55
|
cwd?: string;
|
|
@@ -66,6 +68,11 @@ interface ScanBudget {
|
|
|
66
68
|
exhausted: boolean;
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
interface BlobReplayBudget {
|
|
72
|
+
bytes: number;
|
|
73
|
+
imageReplayWarning?: true;
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
export function validateNativeSessionId(value: unknown): string {
|
|
70
77
|
if (typeof value !== "string" || !NATIVE_SESSION_ID.test(value)) {
|
|
71
78
|
throw new Error("Invalid OMP session identifier");
|
|
@@ -156,6 +163,7 @@ async function yieldToEventLoop(): Promise<void> {
|
|
|
156
163
|
setImmediate(result.resolve);
|
|
157
164
|
await result.promise;
|
|
158
165
|
}
|
|
166
|
+
const UNAVAILABLE_IMAGE_MARKER = "[Image unavailable during session replay]";
|
|
159
167
|
async function readStableFile(
|
|
160
168
|
handle: FileHandle,
|
|
161
169
|
byteLength: number,
|
|
@@ -213,6 +221,7 @@ async function hydrateBlobImageData(
|
|
|
213
221
|
) {
|
|
214
222
|
throw new Error("OMP transcript image blob failed ownership or size validation");
|
|
215
223
|
}
|
|
224
|
+
budget.bytes += stat.size;
|
|
216
225
|
const bytes = await readStableFile(
|
|
217
226
|
handle,
|
|
218
227
|
stat.size,
|
|
@@ -222,7 +231,6 @@ async function hydrateBlobImageData(
|
|
|
222
231
|
if (createHash("sha256").update(bytes).digest("hex") !== hash) {
|
|
223
232
|
throw new Error("OMP transcript image blob failed integrity validation");
|
|
224
233
|
}
|
|
225
|
-
budget.bytes += bytes.byteLength;
|
|
226
234
|
return bytes.toString("base64");
|
|
227
235
|
} finally {
|
|
228
236
|
await handle.close().catch(() => undefined);
|
|
@@ -232,11 +240,13 @@ async function hydrateBlobImageData(
|
|
|
232
240
|
async function hydrateImageParts(
|
|
233
241
|
value: unknown,
|
|
234
242
|
blobDirectory: string,
|
|
235
|
-
budget:
|
|
243
|
+
budget: BlobReplayBudget,
|
|
244
|
+
failureMode: "marker" | "omit",
|
|
236
245
|
signal?: AbortSignal,
|
|
237
246
|
): Promise<unknown> {
|
|
238
247
|
if (!Array.isArray(value)) return value;
|
|
239
248
|
let hydrated: unknown[] | undefined;
|
|
249
|
+
let omitted: boolean[] | undefined;
|
|
240
250
|
for (let index = 0; index < value.length; index += 1) {
|
|
241
251
|
const part = value[index];
|
|
242
252
|
if (
|
|
@@ -251,30 +261,53 @@ async function hydrateImageParts(
|
|
|
251
261
|
) {
|
|
252
262
|
continue;
|
|
253
263
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
264
|
+
try {
|
|
265
|
+
const data = await hydrateBlobImageData(part.data, blobDirectory, budget, signal);
|
|
266
|
+
const mimeType = "mimeType" in part ? part.mimeType : undefined;
|
|
267
|
+
if (typeof mimeType !== "string" || !isValidImagePayload(data, mimeType, data.length)) {
|
|
268
|
+
throw new Error("OMP transcript image blob failed MIME validation");
|
|
269
|
+
}
|
|
270
|
+
hydrated ??= [...value];
|
|
271
|
+
hydrated[index] = { ...part, data };
|
|
272
|
+
} catch {
|
|
273
|
+
signal?.throwIfAborted();
|
|
274
|
+
budget.imageReplayWarning = true;
|
|
275
|
+
hydrated ??= [...value];
|
|
276
|
+
if (failureMode === "marker") {
|
|
277
|
+
hydrated[index] = { type: "text", text: UNAVAILABLE_IMAGE_MARKER };
|
|
278
|
+
} else {
|
|
279
|
+
omitted ??= [];
|
|
280
|
+
omitted[index] = true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
257
283
|
}
|
|
258
|
-
|
|
284
|
+
if (!hydrated) return value;
|
|
285
|
+
return omitted ? hydrated.filter((_, index) => !omitted[index]) : hydrated;
|
|
259
286
|
}
|
|
260
287
|
|
|
261
288
|
async function hydratePersistedMessageImages(
|
|
262
289
|
message: unknown,
|
|
263
290
|
blobDirectory: string | undefined,
|
|
264
|
-
budget:
|
|
291
|
+
budget: BlobReplayBudget,
|
|
265
292
|
signal?: AbortSignal,
|
|
266
293
|
): Promise<unknown> {
|
|
267
294
|
if (!blobDirectory || !message || typeof message !== "object" || Array.isArray(message)) {
|
|
268
295
|
return message;
|
|
269
296
|
}
|
|
270
297
|
const record = message as Record<string, unknown>;
|
|
271
|
-
let content = await hydrateImageParts(record.content, blobDirectory, budget, signal);
|
|
298
|
+
let content = await hydrateImageParts(record.content, blobDirectory, budget, "marker", signal);
|
|
272
299
|
if (content && typeof content === "object" && !Array.isArray(content)) {
|
|
273
300
|
const contentRecord = content as Record<string, unknown>;
|
|
274
|
-
const nested = await hydrateImageParts(
|
|
301
|
+
const nested = await hydrateImageParts(
|
|
302
|
+
contentRecord.content,
|
|
303
|
+
blobDirectory,
|
|
304
|
+
budget,
|
|
305
|
+
"marker",
|
|
306
|
+
signal,
|
|
307
|
+
);
|
|
275
308
|
if (nested !== contentRecord.content) content = { ...contentRecord, content: nested };
|
|
276
309
|
}
|
|
277
|
-
const images = await hydrateImageParts(record.images, blobDirectory, budget, signal);
|
|
310
|
+
const images = await hydrateImageParts(record.images, blobDirectory, budget, "omit", signal);
|
|
278
311
|
if (content === record.content && images === record.images) return message;
|
|
279
312
|
return { ...record, content, images };
|
|
280
313
|
}
|
|
@@ -477,16 +510,19 @@ interface PersistedTranscriptNode {
|
|
|
477
510
|
message?: unknown;
|
|
478
511
|
}
|
|
479
512
|
|
|
480
|
-
function persistedTranscriptMessage(record: Record<string, unknown>, entryId
|
|
513
|
+
function persistedTranscriptMessage(record: Record<string, unknown>, entryId?: string): unknown {
|
|
481
514
|
if (record.type === "message") {
|
|
482
515
|
if (!record.message || typeof record.message !== "object" || Array.isArray(record.message))
|
|
483
516
|
return;
|
|
484
|
-
return {
|
|
517
|
+
return {
|
|
518
|
+
...(record.message as Record<string, unknown>),
|
|
519
|
+
...(entryId ? { entryId } : {}),
|
|
520
|
+
};
|
|
485
521
|
}
|
|
486
522
|
if (record.type !== "custom_message") return;
|
|
487
523
|
return {
|
|
488
524
|
role: "custom",
|
|
489
|
-
entryId,
|
|
525
|
+
...(entryId ? { entryId } : {}),
|
|
490
526
|
customType: record.customType,
|
|
491
527
|
content: record.content,
|
|
492
528
|
display: record.display,
|
|
@@ -620,7 +656,7 @@ export async function readOmpPersistedSessionTranscript(
|
|
|
620
656
|
throw new Error("OMP session transcript exceeds message limits");
|
|
621
657
|
}
|
|
622
658
|
const hydratedMessages: unknown[] = [];
|
|
623
|
-
const blobBudget = { bytes: 0 };
|
|
659
|
+
const blobBudget: BlobReplayBudget = { bytes: 0 };
|
|
624
660
|
for (const message of messages) {
|
|
625
661
|
signal?.throwIfAborted();
|
|
626
662
|
hydratedMessages.push(
|
|
@@ -632,6 +668,7 @@ export async function readOmpPersistedSessionTranscript(
|
|
|
632
668
|
nativeSessionId,
|
|
633
669
|
byteLength: bytes.byteLength,
|
|
634
670
|
messages: hydratedMessages,
|
|
671
|
+
...(blobBudget.imageReplayWarning ? { imageReplayWarning: true as const } : {}),
|
|
635
672
|
};
|
|
636
673
|
} catch (error) {
|
|
637
674
|
if (error instanceof Error) throw error;
|
|
@@ -646,6 +683,7 @@ export async function readOmpPersistedSubagentTranscript(
|
|
|
646
683
|
childTranscriptId: string,
|
|
647
684
|
cwd: string,
|
|
648
685
|
signal?: AbortSignal,
|
|
686
|
+
childSessionFile?: string,
|
|
649
687
|
): Promise<OmpPersistedSubagentTranscript> {
|
|
650
688
|
signal?.throwIfAborted();
|
|
651
689
|
if (
|
|
@@ -653,7 +691,12 @@ export async function readOmpPersistedSubagentTranscript(
|
|
|
653
691
|
!parentSessionFile.endsWith(".jsonl") ||
|
|
654
692
|
parentSessionFile.includes("\0") ||
|
|
655
693
|
!CHILD_TRANSCRIPT_ID.test(childTranscriptId) ||
|
|
656
|
-
basename(childTranscriptId) !== childTranscriptId
|
|
694
|
+
basename(childTranscriptId) !== childTranscriptId ||
|
|
695
|
+
(childSessionFile !== undefined &&
|
|
696
|
+
(!isAbsolute(childSessionFile) ||
|
|
697
|
+
!childSessionFile.endsWith(".jsonl") ||
|
|
698
|
+
childSessionFile.includes("\0") ||
|
|
699
|
+
basename(childSessionFile) !== `${childTranscriptId}.jsonl`))
|
|
657
700
|
) {
|
|
658
701
|
throw new Error("Invalid OMP child transcript descriptor");
|
|
659
702
|
}
|
|
@@ -673,34 +716,53 @@ export async function readOmpPersistedSubagentTranscript(
|
|
|
673
716
|
await parentHandle.close().catch(() => undefined);
|
|
674
717
|
}
|
|
675
718
|
const canonicalParent = await realpath(parentSessionFile);
|
|
676
|
-
const
|
|
677
|
-
const expectedDirectory = canonicalParent.slice(0, -parentExtension.length);
|
|
719
|
+
const expectedDirectory = canonicalParent.slice(0, -".jsonl".length);
|
|
678
720
|
const canonicalDirectory = await realpath(expectedDirectory).catch(() => undefined);
|
|
679
721
|
if (!canonicalDirectory || canonicalDirectory !== expectedDirectory) {
|
|
680
722
|
throw new Error("OMP child transcript directory is not canonically owned by its parent");
|
|
681
723
|
}
|
|
682
|
-
const
|
|
724
|
+
const requestedFile = resolve(
|
|
725
|
+
childSessionFile ?? join(canonicalDirectory, `${childTranscriptId}.jsonl`),
|
|
726
|
+
);
|
|
683
727
|
let handle: FileHandle;
|
|
684
728
|
try {
|
|
685
729
|
handle = await open(
|
|
686
|
-
|
|
730
|
+
requestedFile,
|
|
687
731
|
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0),
|
|
688
732
|
);
|
|
689
733
|
} catch {
|
|
690
734
|
throw new Error("OMP child transcript could not be opened");
|
|
691
735
|
}
|
|
692
736
|
try {
|
|
693
|
-
const [stat, canonicalChild] = await Promise.all([
|
|
737
|
+
const [stat, pathStat, canonicalChild] = await Promise.all([
|
|
738
|
+
handle.stat(),
|
|
739
|
+
lstat(requestedFile),
|
|
740
|
+
realpath(requestedFile),
|
|
741
|
+
]);
|
|
694
742
|
if (
|
|
695
743
|
!stat.isFile() ||
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
744
|
+
!pathStat.isFile() ||
|
|
745
|
+
pathStat.isSymbolicLink() ||
|
|
746
|
+
stat.dev !== pathStat.dev ||
|
|
747
|
+
stat.ino !== pathStat.ino ||
|
|
748
|
+
stat.size > MAX_CHILD_TRANSCRIPT_BYTES
|
|
699
749
|
) {
|
|
700
750
|
throw new Error("OMP child transcript failed ownership validation");
|
|
701
751
|
}
|
|
702
|
-
const
|
|
703
|
-
|
|
752
|
+
const canonicalRelativeChild = relative(canonicalDirectory, canonicalChild);
|
|
753
|
+
if (
|
|
754
|
+
!canonicalRelativeChild ||
|
|
755
|
+
canonicalRelativeChild.startsWith("..") ||
|
|
756
|
+
isAbsolute(canonicalRelativeChild)
|
|
757
|
+
) {
|
|
758
|
+
throw new Error("OMP child transcript failed ownership validation");
|
|
759
|
+
}
|
|
760
|
+
const bytes = await readStableFile(
|
|
761
|
+
handle,
|
|
762
|
+
stat.size,
|
|
763
|
+
signal,
|
|
764
|
+
"OMP child transcript changed while reading",
|
|
765
|
+
);
|
|
704
766
|
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
705
767
|
const messages: unknown[] = [];
|
|
706
768
|
let nativeSessionId: string | undefined;
|
|
@@ -718,15 +780,21 @@ export async function readOmpPersistedSubagentTranscript(
|
|
|
718
780
|
nativeSessionId ??= candidateId;
|
|
719
781
|
if (nativeSessionId !== candidateId)
|
|
720
782
|
throw new Error("OMP child transcript identity changed");
|
|
721
|
-
} else
|
|
783
|
+
} else {
|
|
784
|
+
const entryId =
|
|
785
|
+
typeof record.id === "string" && CHILD_TRANSCRIPT_ID.test(record.id)
|
|
786
|
+
? record.id
|
|
787
|
+
: undefined;
|
|
788
|
+
const message = persistedTranscriptMessage(record, entryId);
|
|
789
|
+
if (message === undefined) continue;
|
|
722
790
|
if (messages.length >= MAX_CHILD_TRANSCRIPT_MESSAGES) {
|
|
723
791
|
throw new Error("OMP child transcript exceeds message limits");
|
|
724
792
|
}
|
|
725
|
-
messages.push(
|
|
793
|
+
messages.push(message);
|
|
726
794
|
}
|
|
727
795
|
}
|
|
728
796
|
if (!nativeSessionId) throw new Error("OMP child transcript is missing session identity");
|
|
729
|
-
return { sessionFile, nativeSessionId, byteLength: bytes.byteLength, messages };
|
|
797
|
+
return { sessionFile: canonicalChild, nativeSessionId, byteLength: bytes.byteLength, messages };
|
|
730
798
|
} catch (error) {
|
|
731
799
|
if (error instanceof Error) throw error;
|
|
732
800
|
throw new Error("OMP child transcript could not be decoded");
|