@dreb/coding-agent 2.63.0 → 2.64.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 +2 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +4 -9
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/output-guard.d.ts +9 -1
- package/dist/core/output-guard.d.ts.map +1 -1
- package/dist/core/output-guard.js +75 -20
- package/dist/core/output-guard.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +12 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +4 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/index.d.ts +1 -0
- package/dist/modes/rpc/index.d.ts.map +1 -1
- package/dist/modes/rpc/index.js +1 -0
- package/dist/modes/rpc/index.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +1 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +21 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-event-projection.d.ts +7 -0
- package/dist/modes/rpc/rpc-event-projection.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-event-projection.js +161 -0
- package/dist/modes/rpc/rpc-event-projection.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +2 -2
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +16 -7
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/compaction.md +16 -0
- package/docs/dashboard.md +16 -2
- package/docs/rpc.md +28 -2
- package/docs/settings.md +5 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc-event-projection.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-event-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAYH;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBhG","sourcesContent":["/**\n * Dashboard-mode RPC event projection.\n *\n * `message_update` events carry two cumulative copies of the growing assistant\n * message: the top-level `message` field and `assistantMessageEvent.partial`.\n * Serializing both for every token makes the child->parent JSONL pipe quadratic\n * in response length and floods the stdout queue (see issue 448).\n *\n * The dashboard never reads those fields: its browser reducer consumes only\n * the delta fields (`delta`, `content`, `toolCall`, `contentIndex`), and its\n * authoritative transcript comes from `message_end` plus `get_dashboard_snapshot`\n * RPC responses. The dashboard's own EventHub already strips the same fields at\n * the browser SSE boundary (projectDashboardEvent); this module applies the same\n * removal one boundary earlier, before JSONL serialization in runRpcMode.\n *\n * Only the quadratic `message_update` fields are removed here. Broader bounding\n * (agent_end messages, tool_execution_update args, retry discardedPartial,\n * images) remains the EventHub's browser-facing concern.\n */\n\nfunction omit(event: Record<string, unknown>, ...keys: string[]): Record<string, unknown> {\n\tconst copy = { ...event };\n\tfor (const key of keys) delete copy[key];\n\treturn copy;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Project a single agent session event for dashboard-mode RPC transport.\n *\n * Unknown event types are returned exactly as received (same reference) so\n * extensions and future event types remain forward-safe. Projected events are\n * shallow copies — the input event is never mutated, because other session\n * subscribers (and the session's own state) share the same object.\n */\nexport function projectDashboardRpcEvent(event: Record<string, unknown>): Record<string, unknown> {\n\tswitch (event.type) {\n\t\tcase \"message_update\": {\n\t\t\tconst projected = omit(event, \"message\");\n\t\t\tconst streamEvent = event.assistantMessageEvent;\n\t\t\treturn isPlainObject(streamEvent)\n\t\t\t\t? { ...projected, assistantMessageEvent: omit(streamEvent, \"partial\") }\n\t\t\t\t: projected;\n\t\t}\n\t\tcase \"background_agent_event\": {\n\t\t\tconst child = event.event;\n\t\t\treturn isPlainObject(child) ? { ...event, event: projectDashboardRpcEvent(child) } : event;\n\t\t}\n\t\tdefault:\n\t\t\treturn event;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rpc-event-projection.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-event-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAcH;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBhG;AA8JD;;;;;GAKG;AACH,wBAAgB,gCAAgC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAM9G","sourcesContent":["/**\n * Dashboard-mode RPC event projection.\n *\n * `message_update` events carry two cumulative copies of the growing assistant\n * message: the top-level `message` field and `assistantMessageEvent.partial`.\n * Serializing both for every token makes the child->parent JSONL pipe quadratic\n * in response length and floods the stdout queue (see issue 448).\n *\n * The dashboard never reads those fields: its browser reducer consumes only\n * the delta fields (`delta`, `content`, `toolCall`, `contentIndex`), and its\n * authoritative transcript comes from `message_end` plus `get_dashboard_snapshot`\n * RPC responses. The dashboard's own EventHub already strips the same fields at\n * the browser SSE boundary (projectDashboardEvent); this module applies the same\n * removal one boundary earlier, before JSONL serialization in runRpcMode.\n *\n * Only the quadratic `message_update` fields are removed here. Broader bounding\n * (agent_end messages, tool_execution_update args, retry discardedPartial,\n * images) remains the EventHub's browser-facing concern.\n */\n\nimport { createHash } from \"node:crypto\";\n\nfunction omit(event: Record<string, unknown>, ...keys: string[]): Record<string, unknown> {\n\tconst copy = { ...event };\n\tfor (const key of keys) delete copy[key];\n\treturn copy;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Project a single agent session event for dashboard-mode RPC transport.\n *\n * Unknown event types are returned exactly as received (same reference) so\n * extensions and future event types remain forward-safe. Projected events are\n * shallow copies — the input event is never mutated, because other session\n * subscribers (and the session's own state) share the same object.\n */\nexport function projectDashboardRpcEvent(event: Record<string, unknown>): Record<string, unknown> {\n\tswitch (event.type) {\n\t\tcase \"message_update\": {\n\t\t\tconst projected = omit(event, \"message\");\n\t\t\tconst streamEvent = event.assistantMessageEvent;\n\t\t\treturn isPlainObject(streamEvent)\n\t\t\t\t? { ...projected, assistantMessageEvent: omit(streamEvent, \"partial\") }\n\t\t\t\t: projected;\n\t\t}\n\t\tcase \"background_agent_event\": {\n\t\t\tconst child = event.event;\n\t\t\treturn isPlainObject(child) ? { ...event, event: projectDashboardRpcEvent(child) } : event;\n\t\t}\n\t\tdefault:\n\t\t\treturn event;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Image dedupe (dashboard mode)\n//\n// Inline image blocks cross the JSONL pipe multiple times per turn: prompt\n// re-emission (message_start + message_end), each tool result\n// (tool_execution_end + message_start + message_end), and the agent_end\n// transcript. A turn with several multi-MiB images easily fills the child\n// stdout queue while the dashboard is busy decoding, and the stdout guard\n// kills the session (issue 495).\n//\n// The dashboard already treats images content-globally: its server projection\n// replaces every inline image with an image_reference whose id is\n// sha256(mimeType + 0x00 + decodedBytes), caches the binary on first sight,\n// and serves later fetches from that cache (recovering from the authoritative\n// transcript when evicted). This projector applies the same reduction one\n// boundary earlier: each unique image crosses stdout at most once per process\n// lifetime; every later occurrence becomes an image_reference the dashboard\n// resolves from its cache. Live events only — command responses\n// (get_messages, get_dashboard_snapshot) always keep full payloads, because\n// they are the authoritative source the dashboard's image recovery reads.\n// ---------------------------------------------------------------------------\n\nconst DASHBOARD_IMAGE_MIME_TYPES = new Set([\"image/png\", \"image/jpeg\", \"image/gif\", \"image/webp\"]);\n\nconst BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;\n\nconst JPEG_EOI = Buffer.from([0xff, 0xd9]);\n\ntype ImageReference = { type: \"image_reference\"; id: string; mimeType: string; size: number };\n\n/**\n * Raster-signature check mirroring the dashboard server's hasRasterSignature\n * (packages/dashboard/src/server/dashboard-images.ts). Keep in sync: the child\n * may only turn a block into a reference when the dashboard's strict decode\n * (decodeDashboardImage) will accept it, or the reference dangles (the cache\n * holds nothing and the authoritative recovery decodes the same bytes and\n * rejects them again).\n */\nfunction hasRasterSignature(mimeType: string, bytes: Uint8Array): boolean {\n\tconst buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\tswitch (mimeType) {\n\t\tcase \"image/png\": {\n\t\t\tconst signature =\n\t\t\t\tbytes.length >= 24 &&\n\t\t\t\t[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((byte, i) => bytes[i] === byte);\n\t\t\treturn (\n\t\t\t\tsignature &&\n\t\t\t\tbuffer.subarray(12, 16).toString(\"ascii\") === \"IHDR\" &&\n\t\t\t\tbuffer.readUInt32BE(16) > 0 &&\n\t\t\t\tbuffer.readUInt32BE(20) > 0 &&\n\t\t\t\tbuffer.indexOf(Buffer.from(\"IEND\"), 24) >= 0\n\t\t\t);\n\t\t}\n\t\tcase \"image/jpeg\": {\n\t\t\tif (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8 || bytes[2] !== 0xff) return false;\n\t\t\t// EOI does not have to be the final byte. Phone JPEGs commonly append\n\t\t\t// auxiliary gain-map, motion-photo, or vendor metadata after the\n\t\t\t// primary image. Browsers and Photon decode the JPEG through EOI and\n\t\t\t// preserve the trailing bytes when the exact original is requested.\n\t\t\treturn buffer.lastIndexOf(JPEG_EOI) >= 2;\n\t\t}\n\t\tcase \"image/gif\": {\n\t\t\tconst header = buffer.subarray(0, 6).toString(\"ascii\");\n\t\t\treturn (\n\t\t\t\tbytes.length >= 14 &&\n\t\t\t\t(header === \"GIF87a\" || header === \"GIF89a\") &&\n\t\t\t\tbuffer.readUInt16LE(6) > 0 &&\n\t\t\t\tbuffer.readUInt16LE(8) > 0 &&\n\t\t\t\tbytes[bytes.length - 1] === 0x3b\n\t\t\t);\n\t\t}\n\t\tcase \"image/webp\": {\n\t\t\tif (\n\t\t\t\tbytes.length < 20 ||\n\t\t\t\tbuffer.subarray(0, 4).toString(\"ascii\") !== \"RIFF\" ||\n\t\t\t\tbuffer.subarray(8, 12).toString(\"ascii\") !== \"WEBP\"\n\t\t\t) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst chunk = buffer.subarray(12, 16).toString(\"ascii\");\n\t\t\treturn (\n\t\t\t\tbuffer.readUInt32LE(4) + 8 <= bytes.length && (chunk === \"VP8 \" || chunk === \"VP8L\" || chunk === \"VP8X\")\n\t\t\t);\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Content identity of an inline image block, mirroring the dashboard server's\n * dashboardImageId (sha256 of mimeType + 0x00 + decoded bytes). The gate is as\n * strict as the dashboard's decodeDashboardImage — allowlisted mimeType,\n * canonical base64 (round trip), and a matching raster signature — so a block\n * the dashboard would reject never becomes a reference: it stays inline at\n * every occurrence and is dropped by the dashboard's strict decode, exactly as\n * before this projection existed (no dangling image_references).\n */\nfunction imageBlockIdentity(mimeType: string, data: string): { id: string; size: number } | undefined {\n\tif (!DASHBOARD_IMAGE_MIME_TYPES.has(mimeType)) return undefined;\n\tif (data.length === 0 || data.length % 4 !== 0 || !BASE64_PATTERN.test(data)) return undefined;\n\tconst bytes = Buffer.from(data, \"base64\");\n\tif (bytes.byteLength === 0) return undefined;\n\tif (bytes.toString(\"base64\") !== data) return undefined; // non-canonical encoding the dashboard rejects\n\tif (!hasRasterSignature(mimeType, bytes)) return undefined; // signature mismatch the dashboard rejects\n\tconst id = createHash(\"sha256\").update(mimeType).update(Uint8Array.of(0)).update(bytes).digest(\"hex\");\n\treturn { id, size: bytes.byteLength };\n}\n\n/**\n * Replace occurrences of already-seen image blocks with their stable\n * reference, keeping the first occurrence of each unique image inline.\n * Returns the input by reference when nothing changed, so events without\n * images keep sharing objects with other session subscribers.\n */\nfunction dedupeImages(node: unknown, seen: Map<string, ImageReference>): unknown {\n\tif (Array.isArray(node)) {\n\t\tlet changed = false;\n\t\tconst result: unknown[] = new Array(node.length);\n\t\tfor (let i = 0; i < node.length; i++) {\n\t\t\tconst original = node[i];\n\t\t\tconst next = dedupeImages(original, seen);\n\t\t\tif (next !== original) changed = true;\n\t\t\tresult[i] = next;\n\t\t}\n\t\treturn changed ? result : node;\n\t}\n\tif (isPlainObject(node)) {\n\t\tif (node.type === \"image\" && typeof node.mimeType === \"string\" && typeof node.data === \"string\") {\n\t\t\tconst identity = imageBlockIdentity(node.mimeType, node.data);\n\t\t\tif (identity) {\n\t\t\t\tconst reference = seen.get(identity.id);\n\t\t\t\tif (reference) return reference;\n\t\t\t\tconst created: ImageReference = {\n\t\t\t\t\ttype: \"image_reference\",\n\t\t\t\t\tid: identity.id,\n\t\t\t\t\tmimeType: node.mimeType,\n\t\t\t\t\tsize: identity.size,\n\t\t\t\t};\n\t\t\t\tseen.set(identity.id, created);\n\t\t\t\treturn node;\n\t\t\t}\n\t\t\treturn node;\n\t\t}\n\t\tlet changed = false;\n\t\tconst result: Record<string, unknown> = {};\n\t\tfor (const key of Object.keys(node)) {\n\t\t\tconst original = node[key];\n\t\t\tconst next = dedupeImages(original, seen);\n\t\t\tif (next !== original) changed = true;\n\t\t\tresult[key] = next;\n\t\t}\n\t\treturn changed ? result : node;\n\t}\n\treturn node;\n}\n\n/**\n * Create the dashboard-mode RPC event projector for one runRpcMode process.\n *\n * Composes the per-event field projection with process-lifetime image dedupe.\n * Dashboard-mode sessions only — generic RPC consumers keep the full protocol.\n */\nexport function createDashboardRpcEventProjector(): (event: Record<string, unknown>) => Record<string, unknown> {\n\tconst seenImageIds = new Map<string, ImageReference>();\n\treturn (event: Record<string, unknown>): Record<string, unknown> => {\n\t\tconst projected = projectDashboardRpcEvent(event);\n\t\treturn dedupeImages(projected, seenImageIds) as Record<string, unknown>;\n\t};\n}\n"]}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* (agent_end messages, tool_execution_update args, retry discardedPartial,
|
|
18
18
|
* images) remains the EventHub's browser-facing concern.
|
|
19
19
|
*/
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
20
21
|
function omit(event, ...keys) {
|
|
21
22
|
const copy = { ...event };
|
|
22
23
|
for (const key of keys)
|
|
@@ -51,4 +52,164 @@ export function projectDashboardRpcEvent(event) {
|
|
|
51
52
|
return event;
|
|
52
53
|
}
|
|
53
54
|
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Image dedupe (dashboard mode)
|
|
57
|
+
//
|
|
58
|
+
// Inline image blocks cross the JSONL pipe multiple times per turn: prompt
|
|
59
|
+
// re-emission (message_start + message_end), each tool result
|
|
60
|
+
// (tool_execution_end + message_start + message_end), and the agent_end
|
|
61
|
+
// transcript. A turn with several multi-MiB images easily fills the child
|
|
62
|
+
// stdout queue while the dashboard is busy decoding, and the stdout guard
|
|
63
|
+
// kills the session (issue 495).
|
|
64
|
+
//
|
|
65
|
+
// The dashboard already treats images content-globally: its server projection
|
|
66
|
+
// replaces every inline image with an image_reference whose id is
|
|
67
|
+
// sha256(mimeType + 0x00 + decodedBytes), caches the binary on first sight,
|
|
68
|
+
// and serves later fetches from that cache (recovering from the authoritative
|
|
69
|
+
// transcript when evicted). This projector applies the same reduction one
|
|
70
|
+
// boundary earlier: each unique image crosses stdout at most once per process
|
|
71
|
+
// lifetime; every later occurrence becomes an image_reference the dashboard
|
|
72
|
+
// resolves from its cache. Live events only — command responses
|
|
73
|
+
// (get_messages, get_dashboard_snapshot) always keep full payloads, because
|
|
74
|
+
// they are the authoritative source the dashboard's image recovery reads.
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
const DASHBOARD_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
77
|
+
const BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
78
|
+
const JPEG_EOI = Buffer.from([0xff, 0xd9]);
|
|
79
|
+
/**
|
|
80
|
+
* Raster-signature check mirroring the dashboard server's hasRasterSignature
|
|
81
|
+
* (packages/dashboard/src/server/dashboard-images.ts). Keep in sync: the child
|
|
82
|
+
* may only turn a block into a reference when the dashboard's strict decode
|
|
83
|
+
* (decodeDashboardImage) will accept it, or the reference dangles (the cache
|
|
84
|
+
* holds nothing and the authoritative recovery decodes the same bytes and
|
|
85
|
+
* rejects them again).
|
|
86
|
+
*/
|
|
87
|
+
function hasRasterSignature(mimeType, bytes) {
|
|
88
|
+
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
89
|
+
switch (mimeType) {
|
|
90
|
+
case "image/png": {
|
|
91
|
+
const signature = bytes.length >= 24 &&
|
|
92
|
+
[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((byte, i) => bytes[i] === byte);
|
|
93
|
+
return (signature &&
|
|
94
|
+
buffer.subarray(12, 16).toString("ascii") === "IHDR" &&
|
|
95
|
+
buffer.readUInt32BE(16) > 0 &&
|
|
96
|
+
buffer.readUInt32BE(20) > 0 &&
|
|
97
|
+
buffer.indexOf(Buffer.from("IEND"), 24) >= 0);
|
|
98
|
+
}
|
|
99
|
+
case "image/jpeg": {
|
|
100
|
+
if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8 || bytes[2] !== 0xff)
|
|
101
|
+
return false;
|
|
102
|
+
// EOI does not have to be the final byte. Phone JPEGs commonly append
|
|
103
|
+
// auxiliary gain-map, motion-photo, or vendor metadata after the
|
|
104
|
+
// primary image. Browsers and Photon decode the JPEG through EOI and
|
|
105
|
+
// preserve the trailing bytes when the exact original is requested.
|
|
106
|
+
return buffer.lastIndexOf(JPEG_EOI) >= 2;
|
|
107
|
+
}
|
|
108
|
+
case "image/gif": {
|
|
109
|
+
const header = buffer.subarray(0, 6).toString("ascii");
|
|
110
|
+
return (bytes.length >= 14 &&
|
|
111
|
+
(header === "GIF87a" || header === "GIF89a") &&
|
|
112
|
+
buffer.readUInt16LE(6) > 0 &&
|
|
113
|
+
buffer.readUInt16LE(8) > 0 &&
|
|
114
|
+
bytes[bytes.length - 1] === 0x3b);
|
|
115
|
+
}
|
|
116
|
+
case "image/webp": {
|
|
117
|
+
if (bytes.length < 20 ||
|
|
118
|
+
buffer.subarray(0, 4).toString("ascii") !== "RIFF" ||
|
|
119
|
+
buffer.subarray(8, 12).toString("ascii") !== "WEBP") {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const chunk = buffer.subarray(12, 16).toString("ascii");
|
|
123
|
+
return (buffer.readUInt32LE(4) + 8 <= bytes.length && (chunk === "VP8 " || chunk === "VP8L" || chunk === "VP8X"));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Content identity of an inline image block, mirroring the dashboard server's
|
|
130
|
+
* dashboardImageId (sha256 of mimeType + 0x00 + decoded bytes). The gate is as
|
|
131
|
+
* strict as the dashboard's decodeDashboardImage — allowlisted mimeType,
|
|
132
|
+
* canonical base64 (round trip), and a matching raster signature — so a block
|
|
133
|
+
* the dashboard would reject never becomes a reference: it stays inline at
|
|
134
|
+
* every occurrence and is dropped by the dashboard's strict decode, exactly as
|
|
135
|
+
* before this projection existed (no dangling image_references).
|
|
136
|
+
*/
|
|
137
|
+
function imageBlockIdentity(mimeType, data) {
|
|
138
|
+
if (!DASHBOARD_IMAGE_MIME_TYPES.has(mimeType))
|
|
139
|
+
return undefined;
|
|
140
|
+
if (data.length === 0 || data.length % 4 !== 0 || !BASE64_PATTERN.test(data))
|
|
141
|
+
return undefined;
|
|
142
|
+
const bytes = Buffer.from(data, "base64");
|
|
143
|
+
if (bytes.byteLength === 0)
|
|
144
|
+
return undefined;
|
|
145
|
+
if (bytes.toString("base64") !== data)
|
|
146
|
+
return undefined; // non-canonical encoding the dashboard rejects
|
|
147
|
+
if (!hasRasterSignature(mimeType, bytes))
|
|
148
|
+
return undefined; // signature mismatch the dashboard rejects
|
|
149
|
+
const id = createHash("sha256").update(mimeType).update(Uint8Array.of(0)).update(bytes).digest("hex");
|
|
150
|
+
return { id, size: bytes.byteLength };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Replace occurrences of already-seen image blocks with their stable
|
|
154
|
+
* reference, keeping the first occurrence of each unique image inline.
|
|
155
|
+
* Returns the input by reference when nothing changed, so events without
|
|
156
|
+
* images keep sharing objects with other session subscribers.
|
|
157
|
+
*/
|
|
158
|
+
function dedupeImages(node, seen) {
|
|
159
|
+
if (Array.isArray(node)) {
|
|
160
|
+
let changed = false;
|
|
161
|
+
const result = new Array(node.length);
|
|
162
|
+
for (let i = 0; i < node.length; i++) {
|
|
163
|
+
const original = node[i];
|
|
164
|
+
const next = dedupeImages(original, seen);
|
|
165
|
+
if (next !== original)
|
|
166
|
+
changed = true;
|
|
167
|
+
result[i] = next;
|
|
168
|
+
}
|
|
169
|
+
return changed ? result : node;
|
|
170
|
+
}
|
|
171
|
+
if (isPlainObject(node)) {
|
|
172
|
+
if (node.type === "image" && typeof node.mimeType === "string" && typeof node.data === "string") {
|
|
173
|
+
const identity = imageBlockIdentity(node.mimeType, node.data);
|
|
174
|
+
if (identity) {
|
|
175
|
+
const reference = seen.get(identity.id);
|
|
176
|
+
if (reference)
|
|
177
|
+
return reference;
|
|
178
|
+
const created = {
|
|
179
|
+
type: "image_reference",
|
|
180
|
+
id: identity.id,
|
|
181
|
+
mimeType: node.mimeType,
|
|
182
|
+
size: identity.size,
|
|
183
|
+
};
|
|
184
|
+
seen.set(identity.id, created);
|
|
185
|
+
return node;
|
|
186
|
+
}
|
|
187
|
+
return node;
|
|
188
|
+
}
|
|
189
|
+
let changed = false;
|
|
190
|
+
const result = {};
|
|
191
|
+
for (const key of Object.keys(node)) {
|
|
192
|
+
const original = node[key];
|
|
193
|
+
const next = dedupeImages(original, seen);
|
|
194
|
+
if (next !== original)
|
|
195
|
+
changed = true;
|
|
196
|
+
result[key] = next;
|
|
197
|
+
}
|
|
198
|
+
return changed ? result : node;
|
|
199
|
+
}
|
|
200
|
+
return node;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Create the dashboard-mode RPC event projector for one runRpcMode process.
|
|
204
|
+
*
|
|
205
|
+
* Composes the per-event field projection with process-lifetime image dedupe.
|
|
206
|
+
* Dashboard-mode sessions only — generic RPC consumers keep the full protocol.
|
|
207
|
+
*/
|
|
208
|
+
export function createDashboardRpcEventProjector() {
|
|
209
|
+
const seenImageIds = new Map();
|
|
210
|
+
return (event) => {
|
|
211
|
+
const projected = projectDashboardRpcEvent(event);
|
|
212
|
+
return dedupeImages(projected, seenImageIds);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
54
215
|
//# sourceMappingURL=rpc-event-projection.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc-event-projection.js","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-event-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,SAAS,IAAI,CAAC,KAA8B,EAAE,GAAG,IAAc,EAA2B;IACzF,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;IAC1B,KAAK,MAAM,GAAG,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;IACzC,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,SAAS,aAAa,CAAC,KAAc,EAAoC;IACxE,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAAA,CAC5E;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAA8B,EAA2B;IACjG,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,gBAAgB,EAAE,CAAC;YACvB,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YACzC,MAAM,WAAW,GAAG,KAAK,CAAC,qBAAqB,CAAC;YAChD,OAAO,aAAa,CAAC,WAAW,CAAC;gBAChC,CAAC,CAAC,EAAE,GAAG,SAAS,EAAE,qBAAqB,EAAE,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,EAAE;gBACvE,CAAC,CAAC,SAAS,CAAC;QACd,CAAC;QACD,KAAK,wBAAwB,EAAE,CAAC;YAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;YAC1B,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,wBAAwB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;QAC5F,CAAC;QACD;YACC,OAAO,KAAK,CAAC;IACf,CAAC;AAAA,CACD","sourcesContent":["/**\n * Dashboard-mode RPC event projection.\n *\n * `message_update` events carry two cumulative copies of the growing assistant\n * message: the top-level `message` field and `assistantMessageEvent.partial`.\n * Serializing both for every token makes the child->parent JSONL pipe quadratic\n * in response length and floods the stdout queue (see issue 448).\n *\n * The dashboard never reads those fields: its browser reducer consumes only\n * the delta fields (`delta`, `content`, `toolCall`, `contentIndex`), and its\n * authoritative transcript comes from `message_end` plus `get_dashboard_snapshot`\n * RPC responses. The dashboard's own EventHub already strips the same fields at\n * the browser SSE boundary (projectDashboardEvent); this module applies the same\n * removal one boundary earlier, before JSONL serialization in runRpcMode.\n *\n * Only the quadratic `message_update` fields are removed here. Broader bounding\n * (agent_end messages, tool_execution_update args, retry discardedPartial,\n * images) remains the EventHub's browser-facing concern.\n */\n\nfunction omit(event: Record<string, unknown>, ...keys: string[]): Record<string, unknown> {\n\tconst copy = { ...event };\n\tfor (const key of keys) delete copy[key];\n\treturn copy;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Project a single agent session event for dashboard-mode RPC transport.\n *\n * Unknown event types are returned exactly as received (same reference) so\n * extensions and future event types remain forward-safe. Projected events are\n * shallow copies — the input event is never mutated, because other session\n * subscribers (and the session's own state) share the same object.\n */\nexport function projectDashboardRpcEvent(event: Record<string, unknown>): Record<string, unknown> {\n\tswitch (event.type) {\n\t\tcase \"message_update\": {\n\t\t\tconst projected = omit(event, \"message\");\n\t\t\tconst streamEvent = event.assistantMessageEvent;\n\t\t\treturn isPlainObject(streamEvent)\n\t\t\t\t? { ...projected, assistantMessageEvent: omit(streamEvent, \"partial\") }\n\t\t\t\t: projected;\n\t\t}\n\t\tcase \"background_agent_event\": {\n\t\t\tconst child = event.event;\n\t\t\treturn isPlainObject(child) ? { ...event, event: projectDashboardRpcEvent(child) } : event;\n\t\t}\n\t\tdefault:\n\t\t\treturn event;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rpc-event-projection.js","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-event-projection.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dashboard-mode RPC event projection.\n *\n * `message_update` events carry two cumulative copies of the growing assistant\n * message: the top-level `message` field and `assistantMessageEvent.partial`.\n * Serializing both for every token makes the child->parent JSONL pipe quadratic\n * in response length and floods the stdout queue (see issue 448).\n *\n * The dashboard never reads those fields: its browser reducer consumes only\n * the delta fields (`delta`, `content`, `toolCall`, `contentIndex`), and its\n * authoritative transcript comes from `message_end` plus `get_dashboard_snapshot`\n * RPC responses. The dashboard's own EventHub already strips the same fields at\n * the browser SSE boundary (projectDashboardEvent); this module applies the same\n * removal one boundary earlier, before JSONL serialization in runRpcMode.\n *\n * Only the quadratic `message_update` fields are removed here. Broader bounding\n * (agent_end messages, tool_execution_update args, retry discardedPartial,\n * images) remains the EventHub's browser-facing concern.\n */\n\nimport { createHash } from \"node:crypto\";\n\nfunction omit(event: Record<string, unknown>, ...keys: string[]): Record<string, unknown> {\n\tconst copy = { ...event };\n\tfor (const key of keys) delete copy[key];\n\treturn copy;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Project a single agent session event for dashboard-mode RPC transport.\n *\n * Unknown event types are returned exactly as received (same reference) so\n * extensions and future event types remain forward-safe. Projected events are\n * shallow copies — the input event is never mutated, because other session\n * subscribers (and the session's own state) share the same object.\n */\nexport function projectDashboardRpcEvent(event: Record<string, unknown>): Record<string, unknown> {\n\tswitch (event.type) {\n\t\tcase \"message_update\": {\n\t\t\tconst projected = omit(event, \"message\");\n\t\t\tconst streamEvent = event.assistantMessageEvent;\n\t\t\treturn isPlainObject(streamEvent)\n\t\t\t\t? { ...projected, assistantMessageEvent: omit(streamEvent, \"partial\") }\n\t\t\t\t: projected;\n\t\t}\n\t\tcase \"background_agent_event\": {\n\t\t\tconst child = event.event;\n\t\t\treturn isPlainObject(child) ? { ...event, event: projectDashboardRpcEvent(child) } : event;\n\t\t}\n\t\tdefault:\n\t\t\treturn event;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Image dedupe (dashboard mode)\n//\n// Inline image blocks cross the JSONL pipe multiple times per turn: prompt\n// re-emission (message_start + message_end), each tool result\n// (tool_execution_end + message_start + message_end), and the agent_end\n// transcript. A turn with several multi-MiB images easily fills the child\n// stdout queue while the dashboard is busy decoding, and the stdout guard\n// kills the session (issue 495).\n//\n// The dashboard already treats images content-globally: its server projection\n// replaces every inline image with an image_reference whose id is\n// sha256(mimeType + 0x00 + decodedBytes), caches the binary on first sight,\n// and serves later fetches from that cache (recovering from the authoritative\n// transcript when evicted). This projector applies the same reduction one\n// boundary earlier: each unique image crosses stdout at most once per process\n// lifetime; every later occurrence becomes an image_reference the dashboard\n// resolves from its cache. Live events only — command responses\n// (get_messages, get_dashboard_snapshot) always keep full payloads, because\n// they are the authoritative source the dashboard's image recovery reads.\n// ---------------------------------------------------------------------------\n\nconst DASHBOARD_IMAGE_MIME_TYPES = new Set([\"image/png\", \"image/jpeg\", \"image/gif\", \"image/webp\"]);\n\nconst BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;\n\nconst JPEG_EOI = Buffer.from([0xff, 0xd9]);\n\ntype ImageReference = { type: \"image_reference\"; id: string; mimeType: string; size: number };\n\n/**\n * Raster-signature check mirroring the dashboard server's hasRasterSignature\n * (packages/dashboard/src/server/dashboard-images.ts). Keep in sync: the child\n * may only turn a block into a reference when the dashboard's strict decode\n * (decodeDashboardImage) will accept it, or the reference dangles (the cache\n * holds nothing and the authoritative recovery decodes the same bytes and\n * rejects them again).\n */\nfunction hasRasterSignature(mimeType: string, bytes: Uint8Array): boolean {\n\tconst buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\tswitch (mimeType) {\n\t\tcase \"image/png\": {\n\t\t\tconst signature =\n\t\t\t\tbytes.length >= 24 &&\n\t\t\t\t[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((byte, i) => bytes[i] === byte);\n\t\t\treturn (\n\t\t\t\tsignature &&\n\t\t\t\tbuffer.subarray(12, 16).toString(\"ascii\") === \"IHDR\" &&\n\t\t\t\tbuffer.readUInt32BE(16) > 0 &&\n\t\t\t\tbuffer.readUInt32BE(20) > 0 &&\n\t\t\t\tbuffer.indexOf(Buffer.from(\"IEND\"), 24) >= 0\n\t\t\t);\n\t\t}\n\t\tcase \"image/jpeg\": {\n\t\t\tif (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8 || bytes[2] !== 0xff) return false;\n\t\t\t// EOI does not have to be the final byte. Phone JPEGs commonly append\n\t\t\t// auxiliary gain-map, motion-photo, or vendor metadata after the\n\t\t\t// primary image. Browsers and Photon decode the JPEG through EOI and\n\t\t\t// preserve the trailing bytes when the exact original is requested.\n\t\t\treturn buffer.lastIndexOf(JPEG_EOI) >= 2;\n\t\t}\n\t\tcase \"image/gif\": {\n\t\t\tconst header = buffer.subarray(0, 6).toString(\"ascii\");\n\t\t\treturn (\n\t\t\t\tbytes.length >= 14 &&\n\t\t\t\t(header === \"GIF87a\" || header === \"GIF89a\") &&\n\t\t\t\tbuffer.readUInt16LE(6) > 0 &&\n\t\t\t\tbuffer.readUInt16LE(8) > 0 &&\n\t\t\t\tbytes[bytes.length - 1] === 0x3b\n\t\t\t);\n\t\t}\n\t\tcase \"image/webp\": {\n\t\t\tif (\n\t\t\t\tbytes.length < 20 ||\n\t\t\t\tbuffer.subarray(0, 4).toString(\"ascii\") !== \"RIFF\" ||\n\t\t\t\tbuffer.subarray(8, 12).toString(\"ascii\") !== \"WEBP\"\n\t\t\t) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst chunk = buffer.subarray(12, 16).toString(\"ascii\");\n\t\t\treturn (\n\t\t\t\tbuffer.readUInt32LE(4) + 8 <= bytes.length && (chunk === \"VP8 \" || chunk === \"VP8L\" || chunk === \"VP8X\")\n\t\t\t);\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Content identity of an inline image block, mirroring the dashboard server's\n * dashboardImageId (sha256 of mimeType + 0x00 + decoded bytes). The gate is as\n * strict as the dashboard's decodeDashboardImage — allowlisted mimeType,\n * canonical base64 (round trip), and a matching raster signature — so a block\n * the dashboard would reject never becomes a reference: it stays inline at\n * every occurrence and is dropped by the dashboard's strict decode, exactly as\n * before this projection existed (no dangling image_references).\n */\nfunction imageBlockIdentity(mimeType: string, data: string): { id: string; size: number } | undefined {\n\tif (!DASHBOARD_IMAGE_MIME_TYPES.has(mimeType)) return undefined;\n\tif (data.length === 0 || data.length % 4 !== 0 || !BASE64_PATTERN.test(data)) return undefined;\n\tconst bytes = Buffer.from(data, \"base64\");\n\tif (bytes.byteLength === 0) return undefined;\n\tif (bytes.toString(\"base64\") !== data) return undefined; // non-canonical encoding the dashboard rejects\n\tif (!hasRasterSignature(mimeType, bytes)) return undefined; // signature mismatch the dashboard rejects\n\tconst id = createHash(\"sha256\").update(mimeType).update(Uint8Array.of(0)).update(bytes).digest(\"hex\");\n\treturn { id, size: bytes.byteLength };\n}\n\n/**\n * Replace occurrences of already-seen image blocks with their stable\n * reference, keeping the first occurrence of each unique image inline.\n * Returns the input by reference when nothing changed, so events without\n * images keep sharing objects with other session subscribers.\n */\nfunction dedupeImages(node: unknown, seen: Map<string, ImageReference>): unknown {\n\tif (Array.isArray(node)) {\n\t\tlet changed = false;\n\t\tconst result: unknown[] = new Array(node.length);\n\t\tfor (let i = 0; i < node.length; i++) {\n\t\t\tconst original = node[i];\n\t\t\tconst next = dedupeImages(original, seen);\n\t\t\tif (next !== original) changed = true;\n\t\t\tresult[i] = next;\n\t\t}\n\t\treturn changed ? result : node;\n\t}\n\tif (isPlainObject(node)) {\n\t\tif (node.type === \"image\" && typeof node.mimeType === \"string\" && typeof node.data === \"string\") {\n\t\t\tconst identity = imageBlockIdentity(node.mimeType, node.data);\n\t\t\tif (identity) {\n\t\t\t\tconst reference = seen.get(identity.id);\n\t\t\t\tif (reference) return reference;\n\t\t\t\tconst created: ImageReference = {\n\t\t\t\t\ttype: \"image_reference\",\n\t\t\t\t\tid: identity.id,\n\t\t\t\t\tmimeType: node.mimeType,\n\t\t\t\t\tsize: identity.size,\n\t\t\t\t};\n\t\t\t\tseen.set(identity.id, created);\n\t\t\t\treturn node;\n\t\t\t}\n\t\t\treturn node;\n\t\t}\n\t\tlet changed = false;\n\t\tconst result: Record<string, unknown> = {};\n\t\tfor (const key of Object.keys(node)) {\n\t\t\tconst original = node[key];\n\t\t\tconst next = dedupeImages(original, seen);\n\t\t\tif (next !== original) changed = true;\n\t\t\tresult[key] = next;\n\t\t}\n\t\treturn changed ? result : node;\n\t}\n\treturn node;\n}\n\n/**\n * Create the dashboard-mode RPC event projector for one runRpcMode process.\n *\n * Composes the per-event field projection with process-lifetime image dedupe.\n * Dashboard-mode sessions only — generic RPC consumers keep the full protocol.\n */\nexport function createDashboardRpcEventProjector(): (event: Record<string, unknown>) => Record<string, unknown> {\n\tconst seenImageIds = new Map<string, ImageReference>();\n\treturn (event: Record<string, unknown>): Record<string, unknown> => {\n\t\tconst projected = projectDashboardRpcEvent(event);\n\t\treturn dedupeImages(projected, seenImageIds) as Record<string, unknown>;\n\t};\n}\n"]}
|
|
@@ -65,9 +65,9 @@ export declare function toRpcBackgroundAgentInfo(a: Readonly<BackgroundAgentInfo
|
|
|
65
65
|
/** Discover available subagent types for RPC clients, sorted by display name. */
|
|
66
66
|
export declare function listAgentTypesForRpc(cwd: string): RpcAgentTypeInfo[];
|
|
67
67
|
/** The slice of SettingsManager the settings RPC handlers need. */
|
|
68
|
-
type SettingsReader = Pick<SettingsManager, "getDefaultProvider" | "getDefaultModel" | "getDefaultThinkingLevel" | "getSteeringMode" | "getFollowUpMode" | "getCompactionEnabled" | "getRetryEnabled" | "getMaxConcurrentSubagents" | "getImageAutoResize" | "getBlockImages" | "getEnableSkillCommands" | "getGlobalContextTrustPolicy" | "getConfiguredTrustedContextFolders" | "getTransport" | "getHideThinkingBlock" | "getAgentModels" | "getGlobalSubagentArbiterSettings" | "getTabTitleSettings" | "getEnabledModels" | "hasProjectEnabledModelsOverride">;
|
|
68
|
+
type SettingsReader = Pick<SettingsManager, "getDefaultProvider" | "getDefaultModel" | "getDefaultThinkingLevel" | "getSteeringMode" | "getFollowUpMode" | "getCompactionEnabled" | "getContinueAfterAutoCompaction" | "getRetryEnabled" | "getMaxConcurrentSubagents" | "getImageAutoResize" | "getBlockImages" | "getEnableSkillCommands" | "getGlobalContextTrustPolicy" | "getConfiguredTrustedContextFolders" | "getTransport" | "getHideThinkingBlock" | "getAgentModels" | "getGlobalSubagentArbiterSettings" | "getTabTitleSettings" | "getEnabledModels" | "hasProjectEnabledModelsOverride">;
|
|
69
69
|
type SettingsRefresher = SettingsReader & Pick<SettingsManager, "reload" | "flush" | "drainErrors" | "hasGlobalSettingsLoadError" | "hasProjectSettingsLoadError">;
|
|
70
|
-
type SettingsWriter = SettingsRefresher & Pick<SettingsManager, "setDefaultModelAndProvider" | "setDefaultThinkingLevel" | "setSteeringMode" | "setFollowUpMode" | "setCompactionEnabled" | "setRetryEnabled" | "setMaxConcurrentSubagents" | "setImageAutoResize" | "setBlockImages" | "setEnableSkillCommands" | "setAutoLoadNestedContext" | "setTrustedContextFolders" | "removeTrustedContextFolder" | "setContextTrust" | "setTransport" | "setHideThinkingBlock" | "setAgentModelsForAgent" | "removeAgentModelsForAgent" | "hasProjectAgentModelOverride" | "setGlobalSubagentArbiterSettings" | "setGlobalTabTitleSettings" | "setEnabledModels">;
|
|
70
|
+
type SettingsWriter = SettingsRefresher & Pick<SettingsManager, "setDefaultModelAndProvider" | "setDefaultThinkingLevel" | "setSteeringMode" | "setFollowUpMode" | "setCompactionEnabled" | "setContinueAfterAutoCompaction" | "setRetryEnabled" | "setMaxConcurrentSubagents" | "setImageAutoResize" | "setBlockImages" | "setEnableSkillCommands" | "setAutoLoadNestedContext" | "setTrustedContextFolders" | "removeTrustedContextFolder" | "setContextTrust" | "setTransport" | "setHideThinkingBlock" | "setAgentModelsForAgent" | "removeAgentModelsForAgent" | "hasProjectAgentModelOverride" | "setGlobalSubagentArbiterSettings" | "setGlobalTabTitleSettings" | "setEnabledModels">;
|
|
71
71
|
/**
|
|
72
72
|
* Handle the `get_settings` RPC command: snapshot the persistent default settings.
|
|
73
73
|
*
|