@omercnet/paseo-omp 0.2.1 → 0.3.0-next.101.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +25 -13
- package/SUPPORT.md +7 -3
- package/TESTING.md +21 -18
- package/client/composer-pill-settings.tsx +157 -0
- package/client/external-url.ts +15 -0
- package/client/mcp-authorization.tsx +169 -0
- package/client/mcp-popover.tsx +155 -0
- package/client/memory-panel.tsx +8 -3
- package/client/memory-popover.tsx +8 -4
- package/client/omp-config-surface.tsx +189 -29
- package/client/omp-plugin-manager.tsx +302 -131
- package/client/omp-store-picker.tsx +89 -0
- package/client/omp-store-state.ts +45 -0
- package/client/paseo-types.ts +9 -0
- package/client/provider-diagnostics-state.ts +18 -7
- package/client/quota-popover.tsx +8 -3
- package/client/quota-state.ts +16 -7
- package/client/sessions-popover.tsx +8 -3
- package/docs/alpha-release-checklist.md +6 -8
- package/docs/configuration.md +8 -4
- package/docs/core-provider-issue-audit.md +3 -2
- 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 +35 -19
- package/index.client.tsx +339 -123
- package/index.server.ts +44 -14
- package/package.json +7 -8
- package/paseo-plugin.json +2 -2
- package/scripts/prepare-dependencies.mjs +24 -0
- package/server/mcp-browser.ts +95 -0
- package/server/memory.ts +2 -2
- package/server/omp-config.ts +16 -7
- package/server/omp-plugins.ts +70 -21
- package/server/omp-settings.ts +232 -24
- package/server/paths.ts +128 -11
- package/server/provider/catalog.ts +3 -4
- package/server/provider/connection.ts +248 -17
- package/server/provider/host-tools.ts +294 -26
- package/server/provider/omp-rpc.ts +499 -72
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/registration.ts +11 -0
- package/server/provider/security.ts +8 -10
- package/server/provider/session-descriptors.ts +340 -1
- package/server/provider/session.ts +716 -249
- package/server/provider/subsessions.ts +25 -2
- package/server/provider/timeline-projector.ts +104 -44
- package/server/provider-diagnostics.ts +122 -36
- package/server/quota.ts +3 -2
- package/server/sessions.ts +2 -2
- package/shared/composer-pill-settings.ts +28 -0
- package/shared/external-url.ts +21 -0
- package/shared/hub.ts +3 -3
- package/shared/mcp.ts +47 -0
- package/shared/memory.ts +2 -1
- package/shared/omp-config.ts +5 -1
- package/shared/omp-plugins.ts +74 -33
- package/shared/omp-settings.ts +8 -1
- package/shared/omp-store.ts +58 -0
- package/shared/provider-diagnostics.ts +12 -3
- package/shared/quota.ts +2 -1
- package/shared/sessions.ts +2 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { constants, type Dir } from "node:fs";
|
|
2
|
-
import { type FileHandle, open, opendir, realpath } from "node:fs/promises";
|
|
3
|
+
import { type FileHandle, lstat, open, opendir, realpath } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
5
6
|
import { ompSessionDir } from "../paths";
|
|
7
|
+
import { isValidImagePayload } from "./image";
|
|
6
8
|
|
|
7
9
|
const MAX_DESCRIPTOR_PREFIX_BYTES = 64 * 1024;
|
|
8
10
|
const MAX_DESCRIPTOR_SUFFIX_BYTES = 64 * 1024;
|
|
@@ -16,6 +18,12 @@ const SCAN_YIELD_INTERVAL = 128;
|
|
|
16
18
|
const MAX_LIST_RESULTS = 500;
|
|
17
19
|
const MAX_CHILD_TRANSCRIPT_BYTES = 16 * 1024 * 1024;
|
|
18
20
|
const MAX_CHILD_TRANSCRIPT_MESSAGES = 100_000;
|
|
21
|
+
const MAX_SESSION_TRANSCRIPT_BYTES = 64 * 1024 * 1024;
|
|
22
|
+
const MAX_SESSION_TRANSCRIPT_ENTRIES = 200_000;
|
|
23
|
+
const MAX_SESSION_TRANSCRIPT_BLOB_BYTES = 16 * 1024 * 1024;
|
|
24
|
+
const MAX_SESSION_IMAGE_BLOB_BYTES = 6 * 1024 * 1024;
|
|
25
|
+
const FILE_READ_CHUNK_BYTES = 64 * 1024;
|
|
26
|
+
const BLOB_REFERENCE = /^blob:sha256:([a-f0-9]{64})$/u;
|
|
19
27
|
const CHILD_TRANSCRIPT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u;
|
|
20
28
|
const NATIVE_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{7,127}$/u;
|
|
21
29
|
|
|
@@ -35,6 +43,14 @@ export interface OmpPersistedSubagentTranscript {
|
|
|
35
43
|
byteLength: number;
|
|
36
44
|
messages: unknown[];
|
|
37
45
|
}
|
|
46
|
+
|
|
47
|
+
export interface OmpPersistedSessionTranscript {
|
|
48
|
+
sessionFile: string;
|
|
49
|
+
nativeSessionId: string;
|
|
50
|
+
byteLength: number;
|
|
51
|
+
messages: unknown[];
|
|
52
|
+
imageReplayWarning?: true;
|
|
53
|
+
}
|
|
38
54
|
export interface OmpSessionListOptions {
|
|
39
55
|
cwd?: string;
|
|
40
56
|
query?: string;
|
|
@@ -52,6 +68,11 @@ interface ScanBudget {
|
|
|
52
68
|
exhausted: boolean;
|
|
53
69
|
}
|
|
54
70
|
|
|
71
|
+
interface BlobReplayBudget {
|
|
72
|
+
bytes: number;
|
|
73
|
+
imageReplayWarning?: true;
|
|
74
|
+
}
|
|
75
|
+
|
|
55
76
|
export function validateNativeSessionId(value: unknown): string {
|
|
56
77
|
if (typeof value !== "string" || !NATIVE_SESSION_ID.test(value)) {
|
|
57
78
|
throw new Error("Invalid OMP session identifier");
|
|
@@ -142,6 +163,154 @@ async function yieldToEventLoop(): Promise<void> {
|
|
|
142
163
|
setImmediate(result.resolve);
|
|
143
164
|
await result.promise;
|
|
144
165
|
}
|
|
166
|
+
const UNAVAILABLE_IMAGE_MARKER = "[Image unavailable during session replay]";
|
|
167
|
+
async function readStableFile(
|
|
168
|
+
handle: FileHandle,
|
|
169
|
+
byteLength: number,
|
|
170
|
+
signal: AbortSignal | undefined,
|
|
171
|
+
changedMessage: string,
|
|
172
|
+
): Promise<Buffer> {
|
|
173
|
+
const bytes = Buffer.allocUnsafe(byteLength);
|
|
174
|
+
let offset = 0;
|
|
175
|
+
while (offset < byteLength) {
|
|
176
|
+
signal?.throwIfAborted();
|
|
177
|
+
const { bytesRead } = await handle.read(
|
|
178
|
+
bytes,
|
|
179
|
+
offset,
|
|
180
|
+
Math.min(FILE_READ_CHUNK_BYTES, byteLength - offset),
|
|
181
|
+
offset,
|
|
182
|
+
);
|
|
183
|
+
if (bytesRead === 0) throw new Error(changedMessage);
|
|
184
|
+
offset += bytesRead;
|
|
185
|
+
}
|
|
186
|
+
signal?.throwIfAborted();
|
|
187
|
+
const extra = Buffer.allocUnsafe(1);
|
|
188
|
+
if ((await handle.read(extra, 0, 1, byteLength)).bytesRead !== 0) throw new Error(changedMessage);
|
|
189
|
+
return bytes;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function hydrateBlobImageData(
|
|
193
|
+
data: string,
|
|
194
|
+
blobDirectory: string,
|
|
195
|
+
budget: { bytes: number },
|
|
196
|
+
signal?: AbortSignal,
|
|
197
|
+
): Promise<string> {
|
|
198
|
+
const match = BLOB_REFERENCE.exec(data);
|
|
199
|
+
if (!match) return data;
|
|
200
|
+
const hash = match[1];
|
|
201
|
+
const blobFile = join(blobDirectory, hash);
|
|
202
|
+
let handle: FileHandle;
|
|
203
|
+
try {
|
|
204
|
+
handle = await open(
|
|
205
|
+
blobFile,
|
|
206
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0),
|
|
207
|
+
);
|
|
208
|
+
} catch {
|
|
209
|
+
throw new Error("OMP transcript image blob could not be opened");
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const [stat, pathStat] = await Promise.all([handle.stat(), lstat(blobFile)]);
|
|
213
|
+
if (
|
|
214
|
+
!stat.isFile() ||
|
|
215
|
+
!pathStat.isFile() ||
|
|
216
|
+
pathStat.isSymbolicLink() ||
|
|
217
|
+
stat.dev !== pathStat.dev ||
|
|
218
|
+
stat.ino !== pathStat.ino ||
|
|
219
|
+
stat.size > MAX_SESSION_IMAGE_BLOB_BYTES ||
|
|
220
|
+
budget.bytes + stat.size > MAX_SESSION_TRANSCRIPT_BLOB_BYTES
|
|
221
|
+
) {
|
|
222
|
+
throw new Error("OMP transcript image blob failed ownership or size validation");
|
|
223
|
+
}
|
|
224
|
+
budget.bytes += stat.size;
|
|
225
|
+
const bytes = await readStableFile(
|
|
226
|
+
handle,
|
|
227
|
+
stat.size,
|
|
228
|
+
signal,
|
|
229
|
+
"OMP transcript image blob changed while reading",
|
|
230
|
+
);
|
|
231
|
+
if (createHash("sha256").update(bytes).digest("hex") !== hash) {
|
|
232
|
+
throw new Error("OMP transcript image blob failed integrity validation");
|
|
233
|
+
}
|
|
234
|
+
return bytes.toString("base64");
|
|
235
|
+
} finally {
|
|
236
|
+
await handle.close().catch(() => undefined);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function hydrateImageParts(
|
|
241
|
+
value: unknown,
|
|
242
|
+
blobDirectory: string,
|
|
243
|
+
budget: BlobReplayBudget,
|
|
244
|
+
failureMode: "marker" | "omit",
|
|
245
|
+
signal?: AbortSignal,
|
|
246
|
+
): Promise<unknown> {
|
|
247
|
+
if (!Array.isArray(value)) return value;
|
|
248
|
+
let hydrated: unknown[] | undefined;
|
|
249
|
+
let omitted: boolean[] | undefined;
|
|
250
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
251
|
+
const part = value[index];
|
|
252
|
+
if (
|
|
253
|
+
!part ||
|
|
254
|
+
typeof part !== "object" ||
|
|
255
|
+
Array.isArray(part) ||
|
|
256
|
+
!("type" in part) ||
|
|
257
|
+
part.type !== "image" ||
|
|
258
|
+
!("data" in part) ||
|
|
259
|
+
typeof part.data !== "string" ||
|
|
260
|
+
!BLOB_REFERENCE.test(part.data)
|
|
261
|
+
) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
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
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!hydrated) return value;
|
|
285
|
+
return omitted ? hydrated.filter((_, index) => !omitted[index]) : hydrated;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function hydratePersistedMessageImages(
|
|
289
|
+
message: unknown,
|
|
290
|
+
blobDirectory: string | undefined,
|
|
291
|
+
budget: BlobReplayBudget,
|
|
292
|
+
signal?: AbortSignal,
|
|
293
|
+
): Promise<unknown> {
|
|
294
|
+
if (!blobDirectory || !message || typeof message !== "object" || Array.isArray(message)) {
|
|
295
|
+
return message;
|
|
296
|
+
}
|
|
297
|
+
const record = message as Record<string, unknown>;
|
|
298
|
+
let content = await hydrateImageParts(record.content, blobDirectory, budget, "marker", signal);
|
|
299
|
+
if (content && typeof content === "object" && !Array.isArray(content)) {
|
|
300
|
+
const contentRecord = content as Record<string, unknown>;
|
|
301
|
+
const nested = await hydrateImageParts(
|
|
302
|
+
contentRecord.content,
|
|
303
|
+
blobDirectory,
|
|
304
|
+
budget,
|
|
305
|
+
"marker",
|
|
306
|
+
signal,
|
|
307
|
+
);
|
|
308
|
+
if (nested !== contentRecord.content) content = { ...contentRecord, content: nested };
|
|
309
|
+
}
|
|
310
|
+
const images = await hydrateImageParts(record.images, blobDirectory, budget, "omit", signal);
|
|
311
|
+
if (content === record.content && images === record.images) return message;
|
|
312
|
+
return { ...record, content, images };
|
|
313
|
+
}
|
|
145
314
|
|
|
146
315
|
async function parseDescriptor(
|
|
147
316
|
file: string,
|
|
@@ -336,6 +505,176 @@ export async function listOmpSessionDescriptors(
|
|
|
336
505
|
return matches;
|
|
337
506
|
}
|
|
338
507
|
|
|
508
|
+
interface PersistedTranscriptNode {
|
|
509
|
+
parentId: string | null;
|
|
510
|
+
message?: unknown;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function persistedTranscriptMessage(record: Record<string, unknown>, entryId: string): unknown {
|
|
514
|
+
if (record.type === "message") {
|
|
515
|
+
if (!record.message || typeof record.message !== "object" || Array.isArray(record.message))
|
|
516
|
+
return;
|
|
517
|
+
return { ...(record.message as Record<string, unknown>), entryId };
|
|
518
|
+
}
|
|
519
|
+
if (record.type !== "custom_message") return;
|
|
520
|
+
return {
|
|
521
|
+
role: "custom",
|
|
522
|
+
entryId,
|
|
523
|
+
customType: record.customType,
|
|
524
|
+
content: record.content,
|
|
525
|
+
display: record.display,
|
|
526
|
+
details: record.details,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Reads the active root-to-leaf display history from an already authorized native transcript.
|
|
532
|
+
* OMP's get_messages endpoint exposes model-safe context and deliberately removes failed turns,
|
|
533
|
+
* so persisted replay must use the journal to preserve user-visible assistant and tool history.
|
|
534
|
+
*/
|
|
535
|
+
export async function readOmpPersistedSessionTranscript(
|
|
536
|
+
sessionFile: string,
|
|
537
|
+
sessionId: string,
|
|
538
|
+
cwd: string,
|
|
539
|
+
signal?: AbortSignal,
|
|
540
|
+
blobDirectory?: string,
|
|
541
|
+
): Promise<OmpPersistedSessionTranscript> {
|
|
542
|
+
signal?.throwIfAborted();
|
|
543
|
+
const expectedSessionId = validateNativeSessionId(sessionId);
|
|
544
|
+
if (
|
|
545
|
+
!isAbsolute(sessionFile) ||
|
|
546
|
+
!sessionFile.endsWith(".jsonl") ||
|
|
547
|
+
sessionFile.includes("\0") ||
|
|
548
|
+
validatedCwd(cwd) !== cwd
|
|
549
|
+
) {
|
|
550
|
+
throw new Error("Invalid OMP session transcript descriptor");
|
|
551
|
+
}
|
|
552
|
+
const expectedFile = resolve(sessionFile);
|
|
553
|
+
let handle: FileHandle;
|
|
554
|
+
try {
|
|
555
|
+
handle = await open(
|
|
556
|
+
expectedFile,
|
|
557
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0),
|
|
558
|
+
);
|
|
559
|
+
} catch {
|
|
560
|
+
throw new Error("OMP session transcript could not be opened");
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const [stat, pathStat, canonicalFile] = await Promise.all([
|
|
564
|
+
handle.stat(),
|
|
565
|
+
lstat(expectedFile),
|
|
566
|
+
realpath(expectedFile),
|
|
567
|
+
]);
|
|
568
|
+
if (
|
|
569
|
+
!stat.isFile() ||
|
|
570
|
+
!pathStat.isFile() ||
|
|
571
|
+
pathStat.isSymbolicLink() ||
|
|
572
|
+
stat.dev !== pathStat.dev ||
|
|
573
|
+
stat.ino !== pathStat.ino ||
|
|
574
|
+
stat.size > MAX_SESSION_TRANSCRIPT_BYTES
|
|
575
|
+
) {
|
|
576
|
+
throw new Error("OMP session transcript failed ownership validation");
|
|
577
|
+
}
|
|
578
|
+
const bytes = await readStableFile(
|
|
579
|
+
handle,
|
|
580
|
+
stat.size,
|
|
581
|
+
signal,
|
|
582
|
+
"OMP session transcript changed while reading",
|
|
583
|
+
);
|
|
584
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
585
|
+
const nodes = new Map<string, PersistedTranscriptNode>();
|
|
586
|
+
let nativeSessionId: string | undefined;
|
|
587
|
+
let leafId: string | undefined;
|
|
588
|
+
let recordsRead = 0;
|
|
589
|
+
for (const line of text.split("\n")) {
|
|
590
|
+
signal?.throwIfAborted();
|
|
591
|
+
recordsRead += 1;
|
|
592
|
+
if (recordsRead % 1_024 === 0) await yieldToEventLoop();
|
|
593
|
+
if (!line.trim()) continue;
|
|
594
|
+
const value: unknown = JSON.parse(line);
|
|
595
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
596
|
+
const record = value as Record<string, unknown>;
|
|
597
|
+
if (record.type === "session") {
|
|
598
|
+
const candidateId = validateNativeSessionId(record.id);
|
|
599
|
+
if (candidateId !== expectedSessionId || validatedCwd(record.cwd) !== cwd) {
|
|
600
|
+
throw new Error("OMP session transcript identity does not match its descriptor");
|
|
601
|
+
}
|
|
602
|
+
nativeSessionId ??= candidateId;
|
|
603
|
+
if (nativeSessionId !== candidateId) {
|
|
604
|
+
throw new Error("OMP session transcript identity changed");
|
|
605
|
+
}
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (typeof record.id !== "string" || !CHILD_TRANSCRIPT_ID.test(record.id)) {
|
|
609
|
+
if (record.type === "message" || record.type === "custom_message") {
|
|
610
|
+
throw new Error("OMP session transcript contains an unlinked message");
|
|
611
|
+
}
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const parentId = record.parentId;
|
|
615
|
+
if (
|
|
616
|
+
parentId !== null &&
|
|
617
|
+
(typeof parentId !== "string" || !CHILD_TRANSCRIPT_ID.test(parentId))
|
|
618
|
+
) {
|
|
619
|
+
throw new Error("OMP session transcript contains an invalid parent identity");
|
|
620
|
+
}
|
|
621
|
+
if (nodes.size >= MAX_SESSION_TRANSCRIPT_ENTRIES) {
|
|
622
|
+
throw new Error("OMP session transcript exceeds entry limits");
|
|
623
|
+
}
|
|
624
|
+
if (nodes.has(record.id))
|
|
625
|
+
throw new Error("OMP session transcript contains duplicate entries");
|
|
626
|
+
nodes.set(record.id, {
|
|
627
|
+
parentId,
|
|
628
|
+
message: persistedTranscriptMessage(record, record.id),
|
|
629
|
+
});
|
|
630
|
+
leafId = record.id;
|
|
631
|
+
}
|
|
632
|
+
if (!nativeSessionId) throw new Error("OMP session transcript is missing session identity");
|
|
633
|
+
|
|
634
|
+
const messages: unknown[] = [];
|
|
635
|
+
const seen = new Set<string>();
|
|
636
|
+
let currentId = leafId;
|
|
637
|
+
let pathEntriesRead = 0;
|
|
638
|
+
while (currentId) {
|
|
639
|
+
pathEntriesRead += 1;
|
|
640
|
+
if (pathEntriesRead % 1_024 === 0) {
|
|
641
|
+
await yieldToEventLoop();
|
|
642
|
+
signal?.throwIfAborted();
|
|
643
|
+
}
|
|
644
|
+
if (seen.has(currentId)) throw new Error("OMP session transcript contains a parent cycle");
|
|
645
|
+
seen.add(currentId);
|
|
646
|
+
const node = nodes.get(currentId);
|
|
647
|
+
if (!node) throw new Error("OMP session transcript contains an unresolved parent");
|
|
648
|
+
if (node.message !== undefined) messages.push(node.message);
|
|
649
|
+
currentId = node.parentId ?? undefined;
|
|
650
|
+
}
|
|
651
|
+
messages.reverse();
|
|
652
|
+
if (messages.length > MAX_CHILD_TRANSCRIPT_MESSAGES) {
|
|
653
|
+
throw new Error("OMP session transcript exceeds message limits");
|
|
654
|
+
}
|
|
655
|
+
const hydratedMessages: unknown[] = [];
|
|
656
|
+
const blobBudget: BlobReplayBudget = { bytes: 0 };
|
|
657
|
+
for (const message of messages) {
|
|
658
|
+
signal?.throwIfAborted();
|
|
659
|
+
hydratedMessages.push(
|
|
660
|
+
await hydratePersistedMessageImages(message, blobDirectory, blobBudget, signal),
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
sessionFile: canonicalFile,
|
|
665
|
+
nativeSessionId,
|
|
666
|
+
byteLength: bytes.byteLength,
|
|
667
|
+
messages: hydratedMessages,
|
|
668
|
+
...(blobBudget.imageReplayWarning ? { imageReplayWarning: true as const } : {}),
|
|
669
|
+
};
|
|
670
|
+
} catch (error) {
|
|
671
|
+
if (error instanceof Error) throw error;
|
|
672
|
+
throw new Error("OMP session transcript could not be decoded");
|
|
673
|
+
} finally {
|
|
674
|
+
await handle.close().catch(() => undefined);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
339
678
|
export async function readOmpPersistedSubagentTranscript(
|
|
340
679
|
parentSessionFile: string,
|
|
341
680
|
childTranscriptId: string,
|