pi-daddy 0.48.3 → 0.49.0

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 (60) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +53 -0
  3. package/dist/cli.d.ts +3 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +18 -0
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/kernel/selected-definitions.d.ts.map +1 -1
  12. package/dist/kernel/selected-definitions.js +6 -3
  13. package/dist/kernel/selected-definitions.js.map +1 -1
  14. package/dist/products/dashboard-cli.d.ts.map +1 -1
  15. package/dist/products/dashboard-cli.js +46 -1
  16. package/dist/products/dashboard-cli.js.map +1 -1
  17. package/dist/products/dashboard-display-controls.d.ts +1 -1
  18. package/dist/products/dashboard-display-controls.d.ts.map +1 -1
  19. package/dist/products/dashboard-display-controls.js +11 -3
  20. package/dist/products/dashboard-display-controls.js.map +1 -1
  21. package/dist/products/dashboard-screen.d.ts +3 -1
  22. package/dist/products/dashboard-screen.d.ts.map +1 -1
  23. package/dist/products/dashboard-screen.js +21 -8
  24. package/dist/products/dashboard-screen.js.map +1 -1
  25. package/dist/products/dashboard-session-client.d.ts +9 -0
  26. package/dist/products/dashboard-session-client.d.ts.map +1 -1
  27. package/dist/products/dashboard-session-client.js.map +1 -1
  28. package/dist/products/dashboard-settings.d.ts +28 -0
  29. package/dist/products/dashboard-settings.d.ts.map +1 -0
  30. package/dist/products/dashboard-settings.js +42 -0
  31. package/dist/products/dashboard-settings.js.map +1 -0
  32. package/dist/products/diagnostic-export.d.ts +23 -0
  33. package/dist/products/diagnostic-export.d.ts.map +1 -0
  34. package/dist/products/diagnostic-export.js +264 -0
  35. package/dist/products/diagnostic-export.js.map +1 -0
  36. package/dist/products/diagnostic-filter.d.ts +7 -0
  37. package/dist/products/diagnostic-filter.d.ts.map +1 -0
  38. package/dist/products/diagnostic-filter.js +135 -0
  39. package/dist/products/diagnostic-filter.js.map +1 -0
  40. package/extensions/dashboard-session-server.ts +17 -2
  41. package/extensions/dashboard-settings.ts +149 -0
  42. package/extensions/delegation-operations.ts +81 -2
  43. package/extensions/execute-child.ts +9 -11
  44. package/extensions/execution-controls.ts +7 -2
  45. package/extensions/grants-command.ts +3 -1
  46. package/extensions/grants.ts +3 -0
  47. package/extensions/reload-environment.ts +3 -0
  48. package/extensions/run-delegation.ts +3 -1
  49. package/extensions/runtime-snapshot.ts +9 -1
  50. package/package.json +1 -1
  51. package/src/cli.ts +24 -1
  52. package/src/index.ts +2 -0
  53. package/src/kernel/selected-definitions.ts +5 -3
  54. package/src/products/dashboard-cli.ts +38 -1
  55. package/src/products/dashboard-display-controls.ts +10 -4
  56. package/src/products/dashboard-screen.ts +21 -10
  57. package/src/products/dashboard-session-client.ts +7 -1
  58. package/src/products/dashboard-settings.ts +72 -0
  59. package/src/products/diagnostic-export.ts +288 -0
  60. package/src/products/diagnostic-filter.ts +140 -0
@@ -0,0 +1,288 @@
1
+ /** Offline, explicit-selection export. Original files are read only; output is private and never training data. */
2
+ import { createHash } from "node:crypto";
3
+ import { constants } from "node:fs";
4
+ import { lstat, mkdir, open, readdir, realpath, writeFile } from "node:fs/promises";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { readBoundedBytes } from "../kernel/bounded-read.ts";
7
+ import { parseRetentionJson } from "../governance/retention-json.ts";
8
+ import { parseExecutionRetentionManifest, RETENTION_CONTENT_KINDS } from "../governance/retention-contract.ts";
9
+ import { filterDiagnostic, type DiagnosticFormat } from "./diagnostic-filter.ts";
10
+
11
+ const MAX_FILE = 16 * 1024 * 1024;
12
+ const MAX_TOTAL = 128 * 1024 * 1024;
13
+ const MAX_SOURCES = 1024;
14
+ const sha256 = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex");
15
+ const object = (x: unknown): x is Record<string, unknown> => x !== null && typeof x === "object" && !Array.isArray(x);
16
+ const pathValue = (x: unknown): x is string =>
17
+ typeof x === "string" && x.length > 0 && x.length <= 4096 && !/[\x00-\x1f]/.test(x);
18
+ const hashValue = (x: unknown): x is string => typeof x === "string" && /^[a-f0-9]{64}$/.test(x);
19
+ interface Source {
20
+ path: string;
21
+ format: DiagnosticFormat;
22
+ sha256?: string;
23
+ }
24
+ export interface DiagnosticSelection {
25
+ version: 1;
26
+ sources: Source[];
27
+ retentionRoots?: string[];
28
+ }
29
+ interface InventoryEntry {
30
+ source: string;
31
+ format: string;
32
+ status: "included" | "missing" | "excluded";
33
+ reason?: string;
34
+ sourceSha256?: string;
35
+ sourceBytes?: number;
36
+ path?: string;
37
+ sha256?: string;
38
+ bytes?: number;
39
+ omissions?: Record<string, number>;
40
+ }
41
+
42
+ export function parseDiagnosticSelection(text: string, base: string): DiagnosticSelection {
43
+ const value = parseRetentionJson(text, 256 * 1024);
44
+ if (
45
+ !object(value) ||
46
+ value.version !== 1 ||
47
+ !Array.isArray(value.sources) ||
48
+ Object.keys(value).some((key) => !["version", "sources", "retentionRoots"].includes(key)) ||
49
+ value.sources.length > MAX_SOURCES ||
50
+ (value.retentionRoots !== undefined && (!Array.isArray(value.retentionRoots) || value.retentionRoots.length > 16))
51
+ )
52
+ throw new Error("invalid diagnostic selection v1");
53
+ const sources = value.sources.map((source): Source => {
54
+ if (
55
+ !object(source) ||
56
+ !pathValue(source.path) ||
57
+ !["session", "json", "jsonl", "text"].includes(String(source.format)) ||
58
+ Object.keys(source).some((key) => !["path", "format", "sha256"].includes(key)) ||
59
+ (source.sha256 !== undefined && !hashValue(source.sha256))
60
+ )
61
+ throw new Error("invalid diagnostic source");
62
+ return {
63
+ path: resolve(base, source.path),
64
+ format: source.format as DiagnosticFormat,
65
+ ...(source.sha256 ? { sha256: source.sha256 as string } : {}),
66
+ };
67
+ });
68
+ const retentionRoots = ((value.retentionRoots ?? []) as unknown[]).map((path) => {
69
+ if (!pathValue(path)) throw new Error("invalid retention root");
70
+ return resolve(base, path);
71
+ });
72
+ return { version: 1, sources, retentionRoots };
73
+ }
74
+
75
+ async function readSource(path: string, limit = MAX_FILE): Promise<Buffer> {
76
+ const before = await lstat(path);
77
+ if (!before.isFile() || (await realpath(path)) !== resolve(path))
78
+ throw new Error("source-must-be-canonical-regular-file");
79
+ const result = await readBoundedBytes(
80
+ path,
81
+ { maxBytes: limit, timeoutMs: 10000 },
82
+ {
83
+ open: (file, flags) => open(file, flags | constants.O_NOFOLLOW),
84
+ read: (handle, buffer, offset, length, position) => handle.read(buffer, offset, length, position),
85
+ close: (handle) => handle.close(),
86
+ },
87
+ );
88
+ if (!result.ok) throw new Error(`source-${result.why}`);
89
+ const after = await lstat(path);
90
+ if (
91
+ !after.isFile() ||
92
+ before.dev !== after.dev ||
93
+ before.ino !== after.ino ||
94
+ before.size !== after.size ||
95
+ before.mtimeMs !== after.mtimeMs ||
96
+ before.ctimeMs !== after.ctimeMs ||
97
+ after.size !== result.bytes.length
98
+ )
99
+ throw new Error("source-changed-during-read");
100
+ return result.bytes;
101
+ }
102
+ const reason = (error: unknown): string => {
103
+ const code = object(error) && typeof error.code === "string" ? error.code : undefined;
104
+ if (code) return code;
105
+ return error instanceof Error && /^source-[a-z-]+$/.test(error.message)
106
+ ? error.message
107
+ : "invalid-or-unreadable-source";
108
+ };
109
+
110
+ export async function exportDiagnostics(selectionPath: string, destination: string) {
111
+ const selectedPath = resolve(selectionPath);
112
+ const selectedBytes = await readSource(selectedPath, 256 * 1024);
113
+ const selection = parseDiagnosticSelection(
114
+ new TextDecoder("utf-8", { fatal: true }).decode(selectedBytes),
115
+ dirname(selectedPath),
116
+ );
117
+ const choices = new Map<string, Source>();
118
+ for (const source of selection.sources) {
119
+ const prior = choices.get(source.path);
120
+ if (prior && (prior.format !== source.format || prior.sha256 !== source.sha256))
121
+ throw new Error("conflicting diagnostic selection for one source");
122
+ choices.set(source.path, source);
123
+ }
124
+ const target = resolve(destination);
125
+ if ((await realpath(dirname(target))) !== dirname(target)) throw new Error("destination parent must be canonical");
126
+ await mkdir(target, { mode: 0o700 }); // Exclusive: never overwrite or remove an existing directory.
127
+ await mkdir(join(target, "objects"), { mode: 0o700 });
128
+ const inventory: InventoryEntry[] = [];
129
+ const objects = new Map<string, number>();
130
+ const seen = new Map<string, { source: Source; row: InventoryEntry }>();
131
+ let total = 0;
132
+ let readTotal = 0;
133
+ const read = async (source: Source) => {
134
+ if (readTotal >= MAX_TOTAL) throw new Error("source-total-size-limit");
135
+ const raw = await readSource(source.path, Math.min(MAX_FILE, MAX_TOTAL - readTotal));
136
+ readTotal += raw.length;
137
+ return raw;
138
+ };
139
+ const add = async (source: Source, expectedBytes?: number, traverse = false): Promise<Buffer | undefined> => {
140
+ const prior = seen.get(source.path);
141
+ const expected = (digest: string | undefined, bytes: number | undefined) => {
142
+ if (source.sha256 && digest !== source.sha256) throw new Error("source-digest-mismatch");
143
+ if (expectedBytes !== undefined && bytes !== expectedBytes) throw new Error("source-size-mismatch");
144
+ };
145
+ if (prior) {
146
+ if (prior.source.format !== source.format) throw new Error("conflicting diagnostic selection for one source");
147
+ if (prior.row.status !== "included") return undefined;
148
+ try {
149
+ expected(prior.row.sourceSha256, prior.row.sourceBytes);
150
+ if (!traverse) return undefined;
151
+ // A selected manifest still needs traversal. Re-read and compare rather than caching all raw payloads.
152
+ const raw = await read(source);
153
+ if (sha256(raw) !== prior.row.sourceSha256 || raw.length !== prior.row.sourceBytes)
154
+ throw new Error("source-changed-before-traversal");
155
+ return raw;
156
+ } catch (error) {
157
+ inventory.push({ source: source.path, format: source.format, status: "missing", reason: reason(error) });
158
+ return undefined;
159
+ }
160
+ }
161
+ if (seen.size >= MAX_SOURCES) throw new Error("diagnostic source count exceeds bounds");
162
+ const row: InventoryEntry = { source: source.path, format: source.format, status: "missing" };
163
+ seen.set(source.path, { source, row });
164
+ inventory.push(row);
165
+ try {
166
+ const raw = await read(source);
167
+ row.sourceSha256 = sha256(raw);
168
+ row.sourceBytes = raw.length;
169
+ expected(row.sourceSha256, row.sourceBytes);
170
+ const filtered = filterDiagnostic(raw, source.format);
171
+ const hash = sha256(filtered.bytes),
172
+ path = `objects/${hash}`;
173
+ if (!objects.has(hash)) {
174
+ if (total + filtered.bytes.length > MAX_TOTAL) throw new Error("source-export-size-limit");
175
+ await writeFile(join(target, path), filtered.bytes, { flag: "wx", mode: 0o600 });
176
+ objects.set(hash, filtered.bytes.length);
177
+ total += filtered.bytes.length;
178
+ }
179
+ Object.assign(row, {
180
+ status: "included",
181
+ path,
182
+ sha256: hash,
183
+ bytes: filtered.bytes.length,
184
+ omissions: filtered.omissions,
185
+ });
186
+ return raw;
187
+ } catch (error) {
188
+ row.reason = reason(error);
189
+ return undefined;
190
+ }
191
+ };
192
+ for (const source of [...choices.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)))
193
+ await add(source);
194
+ const captures: unknown[] = [];
195
+ for (const root of [...new Set(selection.retentionRoots)].sort()) {
196
+ try {
197
+ if ((await realpath(root)) !== root || !(await lstat(root)).isDirectory())
198
+ throw new Error("source-invalid-retention-root");
199
+ const entries = await readdir(root, { withFileTypes: true });
200
+ if (entries.length > MAX_SOURCES) throw new Error("source-directory-size-limit");
201
+ for (const entry of entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
202
+ if (!entry.isDirectory()) continue;
203
+ const manifestPath = join(root, entry.name, "manifest.json");
204
+ const raw = await add({ path: manifestPath, format: "json" }, undefined, true);
205
+ if (!raw) continue;
206
+ let manifest;
207
+ try {
208
+ manifest = parseExecutionRetentionManifest(raw.toString("utf8"));
209
+ } catch {
210
+ inventory.push({
211
+ source: `${manifestPath}#manifest`,
212
+ format: "retention-manifest",
213
+ status: "missing",
214
+ reason: "invalid-retention-manifest",
215
+ });
216
+ captures.push({ source: manifestPath, status: "invalid-retention-manifest" });
217
+ continue;
218
+ }
219
+ const losses = manifest.coverage.losses;
220
+ captures.push({
221
+ source: manifestPath,
222
+ nativeSession: manifest.nativeSession,
223
+ state: manifest.state,
224
+ currentLosses: losses.filter((loss) => loss !== "native-session-read-failed-earlier"),
225
+ recoveredObservations: losses.filter((loss) => loss === "native-session-read-failed-earlier"),
226
+ acceptance: "not-assessed",
227
+ });
228
+ for (const kind of RETENTION_CONTENT_KINDS) {
229
+ const value = manifest.content[kind];
230
+ const format = kind === "session" ? "session" : kind === "checkReceipt" ? "json" : "text";
231
+ const source = `${manifestPath}#${kind}`;
232
+ if (!object(value) || value.status !== "retained") {
233
+ inventory.push({ source, format, status: "missing", reason: "not-retained" });
234
+ continue;
235
+ }
236
+ if (!["session", "checkReceipt", "result"].includes(kind)) {
237
+ inventory.push({
238
+ source,
239
+ format,
240
+ status: "excluded",
241
+ reason: "raw-stream-or-pane",
242
+ sourceSha256: hashValue(value.sha256) ? value.sha256 : undefined,
243
+ });
244
+ continue;
245
+ }
246
+ await add(
247
+ { path: join(dirname(manifestPath), value.path as string), format, sha256: value.sha256! },
248
+ value.bytes as number,
249
+ );
250
+ }
251
+ }
252
+ } catch (error) {
253
+ inventory.push({ source: root, format: "retention-root", status: "missing", reason: reason(error) });
254
+ }
255
+ }
256
+ // Capture metadata is also filtered; an untrusted manifest must not smuggle fields past the projection.
257
+ const manifest = {
258
+ version: 1,
259
+ policy: "pi-daddy-visible-diagnostic-v1",
260
+ selectionSha256: sha256(selectedBytes),
261
+ entries: inventory,
262
+ objects: objects.size,
263
+ exportedBytes: total,
264
+ captures,
265
+ private: true,
266
+ trainingEligible: false,
267
+ exportEligible: false,
268
+ acceptance: "not-assessed",
269
+ limitations: [
270
+ "Not an atomic filesystem snapshot; sources are checked individually.",
271
+ "Known reasoning/environment fields omitted; free-text secret detection is heuristic. Review before sharing.",
272
+ "Raw streams, images, unsupported native entries and unselected files are not exported.",
273
+ "Source hashes identify original bytes; object hashes identify filtered bytes. Neither grants approval or training rights.",
274
+ ],
275
+ };
276
+ const safeManifest = filterDiagnostic(Buffer.from(JSON.stringify(manifest)), "json").bytes;
277
+ await writeFile(join(target, "inventory.json"), safeManifest, { mode: 0o600, flag: "wx" });
278
+ const missing = inventory.filter((entry) => entry.status === "missing").length;
279
+ return {
280
+ directory: target,
281
+ inventory: join(target, "inventory.json"),
282
+ sha256: sha256(safeManifest),
283
+ entries: inventory.length,
284
+ objects: objects.size,
285
+ missing,
286
+ trainingEligible: false,
287
+ };
288
+ }
@@ -0,0 +1,140 @@
1
+ /** Local diagnostic projection. Known private fields are omitted; free-text secret detection is heuristic. */
2
+ import { parseRetentionJson } from "../governance/retention-json.ts";
3
+
4
+ const object = (value: unknown): value is Record<string, unknown> =>
5
+ value !== null && typeof value === "object" && !Array.isArray(value);
6
+ const privateKey = new RegExp(
7
+ "^(?:thinking|reasoning|reasoning_content|reasoning_details|encrypted_content|thoughtSignature|thinkingSignature|signature|" +
8
+ "env|environment|environ|credentials?|authorization|api[-_]?key|token|access[-_]?token|refresh[-_]?token|password|secret)$",
9
+ "i",
10
+ );
11
+ const privateType = /^(?:thinking|reasoning|redacted_thinking|reasoning_content|reasoning_details)$/i;
12
+ export interface DiagnosticFilterResult {
13
+ bytes: Buffer;
14
+ omissions: Record<string, number>;
15
+ }
16
+ export type DiagnosticFormat = "session" | "json" | "jsonl" | "text";
17
+
18
+ export function filterDiagnostic(bytes: Uint8Array, format: DiagnosticFormat): DiagnosticFilterResult {
19
+ const omissions: Record<string, number> = {};
20
+ const note = (reason: string) => (omissions[reason] = (omissions[reason] ?? 0) + 1);
21
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
22
+ const redact = (input: string): string => {
23
+ let output = input.replace(
24
+ /\b(?:sk-(?:or-v1-)?[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g,
25
+ () => {
26
+ note("credential-pattern");
27
+ return "[REDACTED]";
28
+ },
29
+ );
30
+ output = output.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/<>"']+@/gi, (_match, scheme: string) => {
31
+ note("credential-url-userinfo");
32
+ return `${scheme}[REDACTED]@`;
33
+ });
34
+ output = output.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, () => {
35
+ note("credential-pattern");
36
+ return "Bearer [REDACTED]";
37
+ });
38
+ output = output.replace(
39
+ /\b([A-Z][A-Z0-9_]*(?:API_KEY|TOKEN|PASSWORD|SECRET)|API_KEY|TOKEN|PASSWORD|SECRET)(\s*[=:]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;]+)/gi,
40
+ (_match, key: string, separator: string) => {
41
+ note("credential-assignment");
42
+ return `${key}${separator}[REDACTED]`;
43
+ },
44
+ );
45
+ return output;
46
+ };
47
+ const clean = (value: unknown, depth = 0): unknown => {
48
+ if (depth > 64) throw new Error("diagnostic nesting exceeds bounds");
49
+ if (typeof value === "string") {
50
+ // JSON embedded in a packet is a byte-bearing string. Keep it exactly when no redaction is needed.
51
+ if (/^\s*[\[{]/.test(value)) {
52
+ let parsed: unknown;
53
+ try {
54
+ parsed = parseRetentionJson(value, 16 * 1024 * 1024);
55
+ } catch {
56
+ return redact(value);
57
+ }
58
+ const filtered = clean(parsed, depth + 1);
59
+ return JSON.stringify(filtered) === JSON.stringify(parsed) ? value : JSON.stringify(filtered);
60
+ }
61
+ return redact(value);
62
+ }
63
+ if (Array.isArray(value))
64
+ return value.flatMap((item) => {
65
+ if (object(item) && typeof item.type === "string" && privateType.test(item.type)) {
66
+ note("private-reasoning");
67
+ return [];
68
+ }
69
+ return [clean(item, depth + 1)];
70
+ });
71
+ if (!object(value)) return value;
72
+ if (typeof value.type === "string" && privateType.test(value.type)) {
73
+ note("private-reasoning");
74
+ return null;
75
+ }
76
+ const result: Record<string, unknown> = Object.create(null);
77
+ for (const [key, item] of Object.entries(value)) {
78
+ if (privateKey.test(key) || /(?:^|_)(?:API_KEY|ACCESS_TOKEN|REFRESH_TOKEN|PASSWORD|SECRET)$/.test(key)) {
79
+ note("private-field");
80
+ continue;
81
+ }
82
+ result[key] = clean(item, depth + 1);
83
+ }
84
+ return result;
85
+ };
86
+ const pick = (value: Record<string, unknown>, keys: string[]) =>
87
+ Object.fromEntries(keys.filter((key) => Object.hasOwn(value, key)).map((key) => [key, value[key]]));
88
+ const session = (value: unknown): unknown => {
89
+ if (!object(value)) throw new Error("invalid native session record");
90
+ const envelope = pick(value, ["type", "id", "parentId", "timestamp"]);
91
+ if (value.type === "session") return clean({ ...envelope, ...pick(value, ["version", "cwd", "parentSession"]) });
92
+ if (value.type === "message" && object(value.message)) {
93
+ const message = value.message;
94
+ if (!["user", "assistant", "toolResult"].includes(String(message.role))) {
95
+ note("unsupported-message");
96
+ return { ...envelope, omitted: "unsupported-message" };
97
+ }
98
+ const content = typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content;
99
+ if (!Array.isArray(content)) throw new Error("invalid native message content");
100
+ const visible = content.flatMap((block: unknown) => {
101
+ if (!object(block)) throw new Error("invalid native content block");
102
+ if (block.type === "text" && typeof block.text === "string") return [pick(block, ["type", "text"])];
103
+ if (block.type === "toolCall") return [pick(block, ["type", "id", "name", "arguments"])];
104
+ note(typeof block.type === "string" && privateType.test(block.type) ? "private-reasoning" : "non-text-content");
105
+ return [];
106
+ });
107
+ return clean({
108
+ ...envelope,
109
+ message: {
110
+ ...pick(message, ["role", "toolCallId", "toolName", "isError", "provider", "model", "stopReason", "usage"]),
111
+ content: visible,
112
+ },
113
+ });
114
+ }
115
+ if (["model_change", "thinking_level_change", "session_info"].includes(String(value.type)))
116
+ return clean({ ...envelope, ...pick(value, ["provider", "modelId", "thinkingLevel", "name"]) });
117
+ note("unsupported-session-entry");
118
+ return clean({ ...envelope, omitted: "unsupported-session-entry" });
119
+ };
120
+ let output: string;
121
+ if (format === "text") output = redact(text);
122
+ else if (format === "json") output = JSON.stringify(clean(parseRetentionJson(text, 16 * 1024 * 1024))) + "\n";
123
+ else {
124
+ const lines = text.split("\n");
125
+ if (lines.at(-1) === "") lines.pop();
126
+ if (format === "session") {
127
+ const header = parseRetentionJson(lines[0] ?? "", 16 * 1024 * 1024);
128
+ if (!object(header) || header.type !== "session" || header.version !== 3 || typeof header.id !== "string")
129
+ throw new Error("expected native Pi session v3");
130
+ }
131
+ output =
132
+ lines
133
+ .map((line) => {
134
+ const value = parseRetentionJson(line, 16 * 1024 * 1024);
135
+ return JSON.stringify(format === "session" ? session(value) : clean(value));
136
+ })
137
+ .join("\n") + "\n";
138
+ }
139
+ return { bytes: Buffer.from(output), omissions };
140
+ }