@oh-my-pi/snapcompact 15.11.3 → 15.11.6
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 +10 -0
- package/dist/types/snapcompact.d.ts +65 -46
- package/package.json +4 -4
- package/src/snapcompact.ts +124 -91
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.11.4] - 2026-06-12
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Renamed every export to drop the `snapcompact`/`Snapcompact`/`SNAPCOMPACT_` qualifier — the package is meant to be consumed via `import * as snapcompact from "@oh-my-pi/snapcompact"`. Functions: `snapcompactCompact` → `compact`, `renderSnapcompactFrame` → `render`, `snapcompactGeometry` → `geometry`, `normalizeForSnapcompact` → `normalize`, `serializeSnapcompactConversation` → `serializeConversation`, `snapcompactImages` → `images`, `getPreservedSnapcompactArchive` → `getPreservedArchive`, `isSnapcompactShape` → `isShape`, `resolveSnapcompactShape` → `resolveShape`, `createSnapcompactFileOps` → `createFileOps`, `computeSnapcompactFileLists` → `computeFileLists`, `upsertSnapcompactFileOperations` → `upsertFileOperations`. Types: `SnapcompactShape` → `Shape`, `SnapcompactFrame` → `Frame`, `SnapcompactArchive` → `Archive`, `SnapcompactGeometry` → `Geometry`, `SnapcompactOptions` → `Options`, `SnapcompactSerializeOptions` → `SerializeOptions`, `SnapcompactFileOperations` → `FileOperations`, `SnapcompactCompactionDetails`/`Preparation`/`Result` → `CompactionDetails`/`CompactionPreparation`/`CompactionResult`, `SnapcompactConvertToLlm` → `ConvertToLlm`. Constants: `SNAPCOMPACT_X` → `X` (`SHAPES`, `FRAME_SIZE`, `MAX_FRAMES`, `FRAME_TOKEN_ESTIMATE`, `PRESERVE_KEY`, `TOOL_RESULT_MAX_CHARS`, `TOOL_ARG_MAX_CHARS`, `TOOL_CALL_MAX_CHARS`, `TRUNCATE_HEAD_RATIO`, `DIM_ON`, `DIM_OFF`).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added `renderMany()` for paging arbitrary text into snapcompact PNG frames as LLM image blocks, and `frames()` for predicting the frame count without rendering
|
|
14
|
+
|
|
5
15
|
## [15.11.0] - 2026-06-10
|
|
6
16
|
|
|
7
17
|
### Breaking Changes
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import type { Api, ImageContent, Message, Model } from "@oh-my-pi/pi-ai";
|
|
34
34
|
/** One eval-validated frame shape: font, cell, ink, repetition, and size. */
|
|
35
|
-
export interface
|
|
35
|
+
export interface Shape {
|
|
36
36
|
/** Bundled font in the native renderer. */
|
|
37
37
|
font: "5x8" | "8x8";
|
|
38
38
|
/** Target cell advance in pixels; differing from the font's natural cell
|
|
@@ -53,7 +53,7 @@ export interface SnapcompactShape {
|
|
|
53
53
|
imageDetail?: ImageContent["detail"];
|
|
54
54
|
}
|
|
55
55
|
/** Eval-validated shapes, keyed by the provider family they won on. */
|
|
56
|
-
export declare const
|
|
56
|
+
export declare const SHAPES: {
|
|
57
57
|
/** `8x8r-bw`: unscii square, black ink, lines doubled on highlight bands. */
|
|
58
58
|
readonly anthropic: {
|
|
59
59
|
readonly font: "8x8";
|
|
@@ -98,23 +98,23 @@ export declare const SNAPCOMPACT_SHAPES: {
|
|
|
98
98
|
};
|
|
99
99
|
};
|
|
100
100
|
/** Runtime guard for shape overrides loaded from config or preserve data. */
|
|
101
|
-
export declare function
|
|
101
|
+
export declare function isShape(value: unknown): value is Shape;
|
|
102
102
|
/** Pick the eval-optimal frame shape for a provider API. */
|
|
103
|
-
export declare function
|
|
103
|
+
export declare function resolveShape(api?: Api): Shape;
|
|
104
104
|
/** Legacy frame edge in pixels (the 5x8 shape's eval-validated size). New
|
|
105
105
|
* shapes carry their own `frameSize`. */
|
|
106
|
-
export declare const
|
|
106
|
+
export declare const FRAME_SIZE = 2576;
|
|
107
107
|
/** Maximum frames carried on a compaction entry. Oldest frames are dropped
|
|
108
108
|
* first once the budget is exceeded (mirrors how iterative text summaries
|
|
109
109
|
* fade the oldest detail). */
|
|
110
|
-
export declare const
|
|
110
|
+
export declare const MAX_FRAMES = 8;
|
|
111
111
|
/** Conservative per-frame token estimate used for context budgeting
|
|
112
112
|
* (upper bound across shapes: Anthropic bills 1568*1568/750 ≈ 3,278). */
|
|
113
|
-
export declare const
|
|
113
|
+
export declare const FRAME_TOKEN_ESTIMATE = 3300;
|
|
114
114
|
/** Key under `CompactionEntry.preserveData` holding the frame archive. */
|
|
115
|
-
export declare const
|
|
115
|
+
export declare const PRESERVE_KEY = "snapcompact";
|
|
116
116
|
/** One developed snapcompact frame: a base64 PNG plus its reading geometry. */
|
|
117
|
-
export interface
|
|
117
|
+
export interface Frame {
|
|
118
118
|
/** Base64-encoded PNG. */
|
|
119
119
|
data: string;
|
|
120
120
|
mimeType: string;
|
|
@@ -125,37 +125,37 @@ export interface SnapcompactFrame {
|
|
|
125
125
|
/** Characters actually printed onto this frame. */
|
|
126
126
|
chars: number;
|
|
127
127
|
/** Shape metadata (absent on legacy frames, which are 5x8 `sent`). */
|
|
128
|
-
font?:
|
|
129
|
-
variant?:
|
|
128
|
+
font?: Shape["font"];
|
|
129
|
+
variant?: Shape["variant"];
|
|
130
130
|
lineRepeat?: number;
|
|
131
131
|
/** Resolution hint forwarded to the provider when re-attaching. */
|
|
132
132
|
detail?: ImageContent["detail"];
|
|
133
133
|
}
|
|
134
|
-
/** Frame archive persisted under `preserveData[
|
|
135
|
-
export interface
|
|
134
|
+
/** Frame archive persisted under `preserveData[PRESERVE_KEY]`. */
|
|
135
|
+
export interface Archive {
|
|
136
136
|
/** Frames ordered oldest to newest. */
|
|
137
|
-
frames:
|
|
137
|
+
frames: Frame[];
|
|
138
138
|
/** Characters currently readable across all frames. */
|
|
139
139
|
totalChars: number;
|
|
140
140
|
/** Characters dropped so far to respect the frame budget. */
|
|
141
141
|
truncatedChars: number;
|
|
142
142
|
}
|
|
143
|
-
export interface
|
|
143
|
+
export interface Geometry {
|
|
144
144
|
cols: number;
|
|
145
145
|
rows: number;
|
|
146
146
|
/** Characters that fit one frame (cols * rows). */
|
|
147
147
|
capacity: number;
|
|
148
148
|
}
|
|
149
|
-
export interface
|
|
149
|
+
export interface Options<TMessage = Message> extends SerializeOptions {
|
|
150
150
|
/** App-level message transformer (same contract as agent-core's `SummaryOptions.convertToLlm`). */
|
|
151
|
-
convertToLlm?:
|
|
151
|
+
convertToLlm?: ConvertToLlm<TMessage>;
|
|
152
152
|
/** Model whose provider API selects the frame shape. */
|
|
153
153
|
model?: Pick<Model, "api">;
|
|
154
154
|
/** Explicit shape override; wins over `model`. */
|
|
155
|
-
shape?:
|
|
155
|
+
shape?: Shape;
|
|
156
156
|
/** Frame edge in pixels. Defaults to the shape's `frameSize`. */
|
|
157
157
|
frameSize?: number;
|
|
158
|
-
/** Frame budget. Defaults to {@link
|
|
158
|
+
/** Frame budget. Defaults to {@link MAX_FRAMES}. */
|
|
159
159
|
maxFrames?: number;
|
|
160
160
|
}
|
|
161
161
|
/** Result of rendering one frame. */
|
|
@@ -167,16 +167,16 @@ export interface RenderedFrame {
|
|
|
167
167
|
/** Characters printed (ink toggles excluded; input may be shorter than capacity). */
|
|
168
168
|
chars: number;
|
|
169
169
|
}
|
|
170
|
-
export interface
|
|
170
|
+
export interface FileOperations {
|
|
171
171
|
read: Set<string>;
|
|
172
172
|
written: Set<string>;
|
|
173
173
|
edited: Set<string>;
|
|
174
174
|
}
|
|
175
|
-
export interface
|
|
175
|
+
export interface CompactionDetails {
|
|
176
176
|
readFiles: string[];
|
|
177
177
|
modifiedFiles: string[];
|
|
178
178
|
}
|
|
179
|
-
export interface
|
|
179
|
+
export interface CompactionPreparation<TMessage = Message> {
|
|
180
180
|
/** UUID of first entry to keep. */
|
|
181
181
|
firstKeptEntryId: string;
|
|
182
182
|
/** Messages that will be archived and discarded. */
|
|
@@ -189,9 +189,9 @@ export interface SnapcompactCompactionPreparation<TMessage = Message> {
|
|
|
189
189
|
/** Preserved opaque compaction payload from the previous compaction, if any. */
|
|
190
190
|
previousPreserveData?: Record<string, unknown>;
|
|
191
191
|
/** File operations extracted by the host agent. */
|
|
192
|
-
fileOps:
|
|
192
|
+
fileOps: FileOperations;
|
|
193
193
|
}
|
|
194
|
-
export interface
|
|
194
|
+
export interface CompactionResult<T = CompactionDetails> {
|
|
195
195
|
summary: string;
|
|
196
196
|
shortSummary?: string;
|
|
197
197
|
firstKeptEntryId: string;
|
|
@@ -199,54 +199,73 @@ export interface SnapcompactCompactionResult<T = SnapcompactCompactionDetails> {
|
|
|
199
199
|
details?: T;
|
|
200
200
|
preserveData?: Record<string, unknown>;
|
|
201
201
|
}
|
|
202
|
-
export type
|
|
203
|
-
export declare function
|
|
204
|
-
export declare function
|
|
205
|
-
export declare function
|
|
202
|
+
export type ConvertToLlm<TMessage = Message> = (messages: TMessage[]) => Message[];
|
|
203
|
+
export declare function createFileOps(): FileOperations;
|
|
204
|
+
export declare function computeFileLists(fileOps: FileOperations): CompactionDetails;
|
|
205
|
+
export declare function upsertFileOperations(summary: string, readFiles: string[], modifiedFiles: string[], readSet?: ReadonlySet<string>): string;
|
|
206
206
|
/** Default per-tool-result character cap in serialized history. */
|
|
207
|
-
export declare const
|
|
207
|
+
export declare const TOOL_RESULT_MAX_CHARS = 2000;
|
|
208
208
|
/** Default per-argument-value character cap inside serialized tool calls
|
|
209
209
|
* (write/edit bodies otherwise dump whole files into the archive). */
|
|
210
|
-
export declare const
|
|
210
|
+
export declare const TOOL_ARG_MAX_CHARS = 500;
|
|
211
211
|
/** Default character cap across one tool call's full serialized argument list. */
|
|
212
|
-
export declare const
|
|
212
|
+
export declare const TOOL_CALL_MAX_CHARS = 2000;
|
|
213
213
|
/** Default fraction of a truncation budget spent on the head; the remainder
|
|
214
214
|
* keeps the tail, where command errors and test failures usually land. */
|
|
215
|
-
export declare const
|
|
215
|
+
export declare const TRUNCATE_HEAD_RATIO = 0.6;
|
|
216
216
|
/** Zero-width ink toggles understood by the native renderer (shift-out/in):
|
|
217
217
|
* text between them prints in dim gray ink without occupying a cell. */
|
|
218
|
-
export declare const
|
|
219
|
-
export declare const
|
|
218
|
+
export declare const DIM_ON = "\u000E";
|
|
219
|
+
export declare const DIM_OFF = "\u000F";
|
|
220
220
|
/** Character budgets applied while serializing discarded history for frame
|
|
221
221
|
* rendering. Pass `Infinity` to disable an individual cap. */
|
|
222
|
-
export interface
|
|
223
|
-
/** Per-tool-result cap. Defaults to {@link
|
|
222
|
+
export interface SerializeOptions {
|
|
223
|
+
/** Per-tool-result cap. Defaults to {@link TOOL_RESULT_MAX_CHARS}. */
|
|
224
224
|
toolResultMaxChars?: number;
|
|
225
|
-
/** Per-argument-value cap. Defaults to {@link
|
|
225
|
+
/** Per-argument-value cap. Defaults to {@link TOOL_ARG_MAX_CHARS}. */
|
|
226
226
|
toolArgMaxChars?: number;
|
|
227
|
-
/** Whole-argument-list cap per call. Defaults to {@link
|
|
227
|
+
/** Whole-argument-list cap per call. Defaults to {@link TOOL_CALL_MAX_CHARS}. */
|
|
228
228
|
toolCallMaxChars?: number;
|
|
229
|
-
/** Head share of each budget, clamped to [0, 1]. Defaults to {@link
|
|
229
|
+
/** Head share of each budget, clamped to [0, 1]. Defaults to {@link TRUNCATE_HEAD_RATIO}. */
|
|
230
230
|
truncateHeadRatio?: number;
|
|
231
231
|
/** Print tool-result text in dim gray ink so archived conversation reads
|
|
232
232
|
* louder than archived tool noise. Defaults to `true`. */
|
|
233
233
|
dimToolResults?: boolean;
|
|
234
234
|
}
|
|
235
|
-
export declare function
|
|
235
|
+
export declare function serializeConversation(messages: Message[], options?: SerializeOptions): string;
|
|
236
236
|
/**
|
|
237
237
|
* Prepare text for printing: collapse whitespace runs (incl. newlines) to
|
|
238
238
|
* single spaces — the eval's "paragraph breaks collapsed to spaces" format —
|
|
239
239
|
* then fold everything outside the fonts' ASCII + Latin-1 coverage to ASCII
|
|
240
240
|
* approximations (`?` as the last resort).
|
|
241
241
|
*/
|
|
242
|
-
export declare function
|
|
243
|
-
export declare function
|
|
242
|
+
export declare function normalize(text: string): string;
|
|
243
|
+
export declare function geometry(shape: Shape, size?: number): Geometry;
|
|
244
244
|
/** Render one snapcompact frame from already-normalized text. */
|
|
245
|
-
export declare function
|
|
245
|
+
export declare function render(text: string, shape: Shape, size?: number): RenderedFrame;
|
|
246
|
+
/** Options for {@link renderMany} and {@link frames}. */
|
|
247
|
+
export interface RenderManyOptions {
|
|
248
|
+
/** Explicit shape; wins over `model`. */
|
|
249
|
+
shape?: Shape;
|
|
250
|
+
/** Model whose `api` selects the eval-optimal shape. */
|
|
251
|
+
model?: Pick<Model, "api">;
|
|
252
|
+
/** Frame edge in px; defaults to the shape's `frameSize`. */
|
|
253
|
+
frameSize?: number;
|
|
254
|
+
/** Hard cap on frames produced; omit for unbounded (caller decides usage). */
|
|
255
|
+
maxFrames?: number;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Render arbitrary text into snapcompact PNG frames as LLM image blocks
|
|
259
|
+
* (first page first). Synchronous: safe to call from per-request transforms.
|
|
260
|
+
* Empty/whitespace-only input yields no frames.
|
|
261
|
+
*/
|
|
262
|
+
export declare function renderMany(text: string, options?: RenderManyOptions): ImageContent[];
|
|
263
|
+
/** Frames needed to hold `text` at the given shape/size, without rendering. */
|
|
264
|
+
export declare function frames(text: string, options?: Pick<RenderManyOptions, "shape" | "model" | "frameSize">): number;
|
|
246
265
|
/** Validate and extract a persisted frame archive from `preserveData`. */
|
|
247
|
-
export declare function
|
|
266
|
+
export declare function getPreservedArchive(preserveData: Record<string, unknown> | undefined): Archive | undefined;
|
|
248
267
|
/** Convert archive frames into LLM image blocks (oldest first). */
|
|
249
|
-
export declare function
|
|
268
|
+
export declare function images(archive: Archive): ImageContent[];
|
|
250
269
|
/**
|
|
251
270
|
* Run a snapcompact compaction over prepared messages. Fully local: serializes
|
|
252
271
|
* the discarded history, prints it onto PNG frames in the provider-optimal
|
|
@@ -262,4 +281,4 @@ export declare function snapcompactImages(archive: SnapcompactArchive): ImageCon
|
|
|
262
281
|
* If the previous compaction was text-based, its summary is printed at the
|
|
263
282
|
* head of the frame archive as `[Summary of earlier history]` so no continuity is lost.
|
|
264
283
|
*/
|
|
265
|
-
export declare function
|
|
284
|
+
export declare function compact<TMessage = Message>(preparation: CompactionPreparation<TMessage>, options?: Options<TMessage>): Promise<CompactionResult>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/snapcompact",
|
|
4
|
-
"version": "15.11.
|
|
4
|
+
"version": "15.11.6",
|
|
5
5
|
"description": "Bitmap-frame context compression for vision-capable LLMs",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-ai": "15.11.
|
|
35
|
-
"@oh-my-pi/pi-natives": "15.11.
|
|
36
|
-
"@oh-my-pi/pi-utils": "15.11.
|
|
34
|
+
"@oh-my-pi/pi-ai": "15.11.6",
|
|
35
|
+
"@oh-my-pi/pi-natives": "15.11.6",
|
|
36
|
+
"@oh-my-pi/pi-utils": "15.11.6"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/bun": "^1.3.14"
|
package/src/snapcompact.ts
CHANGED
|
@@ -42,7 +42,7 @@ import snapcompactSummaryPrompt from "./prompts/snapcompact-summary.md" with { t
|
|
|
42
42
|
// ============================================================================
|
|
43
43
|
|
|
44
44
|
/** One eval-validated frame shape: font, cell, ink, repetition, and size. */
|
|
45
|
-
export interface
|
|
45
|
+
export interface Shape {
|
|
46
46
|
/** Bundled font in the native renderer. */
|
|
47
47
|
font: "5x8" | "8x8";
|
|
48
48
|
/** Target cell advance in pixels; differing from the font's natural cell
|
|
@@ -64,7 +64,7 @@ export interface SnapcompactShape {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** Eval-validated shapes, keyed by the provider family they won on. */
|
|
67
|
-
export const
|
|
67
|
+
export const SHAPES = {
|
|
68
68
|
/** `8x8r-bw`: unscii square, black ink, lines doubled on highlight bands. */
|
|
69
69
|
anthropic: {
|
|
70
70
|
font: "8x8",
|
|
@@ -107,10 +107,10 @@ export const SNAPCOMPACT_SHAPES = {
|
|
|
107
107
|
frameSize: 2576,
|
|
108
108
|
frameTokenEstimate: 3300,
|
|
109
109
|
},
|
|
110
|
-
} as const satisfies Record<string,
|
|
110
|
+
} as const satisfies Record<string, Shape>;
|
|
111
111
|
|
|
112
112
|
/** Runtime guard for shape overrides loaded from config or preserve data. */
|
|
113
|
-
export function
|
|
113
|
+
export function isShape(value: unknown): value is Shape {
|
|
114
114
|
if (!value || typeof value !== "object") return false;
|
|
115
115
|
const shape = value as Record<string, unknown>;
|
|
116
116
|
const font = shape.font;
|
|
@@ -134,21 +134,21 @@ export function isSnapcompactShape(value: unknown): value is SnapcompactShape {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/** Pick the eval-optimal frame shape for a provider API. */
|
|
137
|
-
export function
|
|
137
|
+
export function resolveShape(api?: Api): Shape {
|
|
138
138
|
switch (api) {
|
|
139
139
|
case "openai-completions":
|
|
140
140
|
case "openai-responses":
|
|
141
141
|
case "openai-codex-responses":
|
|
142
142
|
case "azure-openai-responses":
|
|
143
|
-
return
|
|
143
|
+
return SHAPES.openaiDense;
|
|
144
144
|
case "google-generative-ai":
|
|
145
145
|
case "google-gemini-cli":
|
|
146
146
|
case "google-vertex":
|
|
147
|
-
return
|
|
147
|
+
return SHAPES.google;
|
|
148
148
|
default:
|
|
149
149
|
// anthropic-messages, bedrock-converse-stream, and anything unknown:
|
|
150
150
|
// the plain repeated grid is the most refusal-robust reader shape.
|
|
151
|
-
return
|
|
151
|
+
return SHAPES.anthropic;
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -158,26 +158,26 @@ export function resolveSnapcompactShape(api?: Api): SnapcompactShape {
|
|
|
158
158
|
|
|
159
159
|
/** Legacy frame edge in pixels (the 5x8 shape's eval-validated size). New
|
|
160
160
|
* shapes carry their own `frameSize`. */
|
|
161
|
-
export const
|
|
161
|
+
export const FRAME_SIZE = 2576;
|
|
162
162
|
|
|
163
163
|
/** Maximum frames carried on a compaction entry. Oldest frames are dropped
|
|
164
164
|
* first once the budget is exceeded (mirrors how iterative text summaries
|
|
165
165
|
* fade the oldest detail). */
|
|
166
|
-
export const
|
|
166
|
+
export const MAX_FRAMES = 8;
|
|
167
167
|
|
|
168
168
|
/** Conservative per-frame token estimate used for context budgeting
|
|
169
169
|
* (upper bound across shapes: Anthropic bills 1568*1568/750 ≈ 3,278). */
|
|
170
|
-
export const
|
|
170
|
+
export const FRAME_TOKEN_ESTIMATE = 3300;
|
|
171
171
|
|
|
172
172
|
/** Key under `CompactionEntry.preserveData` holding the frame archive. */
|
|
173
|
-
export const
|
|
173
|
+
export const PRESERVE_KEY = "snapcompact";
|
|
174
174
|
|
|
175
175
|
// ============================================================================
|
|
176
176
|
// Types
|
|
177
177
|
// ============================================================================
|
|
178
178
|
|
|
179
179
|
/** One developed snapcompact frame: a base64 PNG plus its reading geometry. */
|
|
180
|
-
export interface
|
|
180
|
+
export interface Frame {
|
|
181
181
|
/** Base64-encoded PNG. */
|
|
182
182
|
data: string;
|
|
183
183
|
mimeType: string;
|
|
@@ -188,40 +188,40 @@ export interface SnapcompactFrame {
|
|
|
188
188
|
/** Characters actually printed onto this frame. */
|
|
189
189
|
chars: number;
|
|
190
190
|
/** Shape metadata (absent on legacy frames, which are 5x8 `sent`). */
|
|
191
|
-
font?:
|
|
192
|
-
variant?:
|
|
191
|
+
font?: Shape["font"];
|
|
192
|
+
variant?: Shape["variant"];
|
|
193
193
|
lineRepeat?: number;
|
|
194
194
|
/** Resolution hint forwarded to the provider when re-attaching. */
|
|
195
195
|
detail?: ImageContent["detail"];
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
/** Frame archive persisted under `preserveData[
|
|
199
|
-
export interface
|
|
198
|
+
/** Frame archive persisted under `preserveData[PRESERVE_KEY]`. */
|
|
199
|
+
export interface Archive {
|
|
200
200
|
/** Frames ordered oldest to newest. */
|
|
201
|
-
frames:
|
|
201
|
+
frames: Frame[];
|
|
202
202
|
/** Characters currently readable across all frames. */
|
|
203
203
|
totalChars: number;
|
|
204
204
|
/** Characters dropped so far to respect the frame budget. */
|
|
205
205
|
truncatedChars: number;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
export interface
|
|
208
|
+
export interface Geometry {
|
|
209
209
|
cols: number;
|
|
210
210
|
rows: number;
|
|
211
211
|
/** Characters that fit one frame (cols * rows). */
|
|
212
212
|
capacity: number;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
export interface
|
|
215
|
+
export interface Options<TMessage = Message> extends SerializeOptions {
|
|
216
216
|
/** App-level message transformer (same contract as agent-core's `SummaryOptions.convertToLlm`). */
|
|
217
|
-
convertToLlm?:
|
|
217
|
+
convertToLlm?: ConvertToLlm<TMessage>;
|
|
218
218
|
/** Model whose provider API selects the frame shape. */
|
|
219
219
|
model?: Pick<Model, "api">;
|
|
220
220
|
/** Explicit shape override; wins over `model`. */
|
|
221
|
-
shape?:
|
|
221
|
+
shape?: Shape;
|
|
222
222
|
/** Frame edge in pixels. Defaults to the shape's `frameSize`. */
|
|
223
223
|
frameSize?: number;
|
|
224
|
-
/** Frame budget. Defaults to {@link
|
|
224
|
+
/** Frame budget. Defaults to {@link MAX_FRAMES}. */
|
|
225
225
|
maxFrames?: number;
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -239,18 +239,18 @@ export interface RenderedFrame {
|
|
|
239
239
|
// Compaction data contracts
|
|
240
240
|
// ============================================================================
|
|
241
241
|
|
|
242
|
-
export interface
|
|
242
|
+
export interface FileOperations {
|
|
243
243
|
read: Set<string>;
|
|
244
244
|
written: Set<string>;
|
|
245
245
|
edited: Set<string>;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
export interface
|
|
248
|
+
export interface CompactionDetails {
|
|
249
249
|
readFiles: string[];
|
|
250
250
|
modifiedFiles: string[];
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
export interface
|
|
253
|
+
export interface CompactionPreparation<TMessage = Message> {
|
|
254
254
|
/** UUID of first entry to keep. */
|
|
255
255
|
firstKeptEntryId: string;
|
|
256
256
|
/** Messages that will be archived and discarded. */
|
|
@@ -263,10 +263,10 @@ export interface SnapcompactCompactionPreparation<TMessage = Message> {
|
|
|
263
263
|
/** Preserved opaque compaction payload from the previous compaction, if any. */
|
|
264
264
|
previousPreserveData?: Record<string, unknown>;
|
|
265
265
|
/** File operations extracted by the host agent. */
|
|
266
|
-
fileOps:
|
|
266
|
+
fileOps: FileOperations;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
export interface
|
|
269
|
+
export interface CompactionResult<T = CompactionDetails> {
|
|
270
270
|
summary: string;
|
|
271
271
|
shortSummary?: string;
|
|
272
272
|
firstKeptEntryId: string;
|
|
@@ -275,7 +275,7 @@ export interface SnapcompactCompactionResult<T = SnapcompactCompactionDetails> {
|
|
|
275
275
|
preserveData?: Record<string, unknown>;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
export type
|
|
278
|
+
export type ConvertToLlm<TMessage = Message> = (messages: TMessage[]) => Message[];
|
|
279
279
|
|
|
280
280
|
function defaultConvertToLlm<TMessage>(messages: TMessage[]): Message[] {
|
|
281
281
|
return messages as unknown as Message[];
|
|
@@ -285,7 +285,7 @@ function defaultConvertToLlm<TMessage>(messages: TMessage[]): Message[] {
|
|
|
285
285
|
// File operation helpers
|
|
286
286
|
// ============================================================================
|
|
287
287
|
|
|
288
|
-
export function
|
|
288
|
+
export function createFileOps(): FileOperations {
|
|
289
289
|
return {
|
|
290
290
|
read: new Set(),
|
|
291
291
|
written: new Set(),
|
|
@@ -293,7 +293,7 @@ export function createSnapcompactFileOps(): SnapcompactFileOperations {
|
|
|
293
293
|
};
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
export function
|
|
296
|
+
export function computeFileLists(fileOps: FileOperations): CompactionDetails {
|
|
297
297
|
const modified = new Set([...fileOps.edited, ...fileOps.written]);
|
|
298
298
|
const readFiles = [...fileOps.read].filter(file => !modified.has(file)).sort();
|
|
299
299
|
const modifiedFiles = [...modified].sort();
|
|
@@ -331,7 +331,7 @@ function formatFileOperations(readFiles: string[], modifiedFiles: string[], read
|
|
|
331
331
|
return prompt.render(fileOperationsTemplate, { files });
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
export function
|
|
334
|
+
export function upsertFileOperations(
|
|
335
335
|
summary: string,
|
|
336
336
|
readFiles: string[],
|
|
337
337
|
modifiedFiles: string[],
|
|
@@ -349,34 +349,34 @@ export function upsertSnapcompactFileOperations(
|
|
|
349
349
|
// ============================================================================
|
|
350
350
|
|
|
351
351
|
/** Default per-tool-result character cap in serialized history. */
|
|
352
|
-
export const
|
|
352
|
+
export const TOOL_RESULT_MAX_CHARS = 2000;
|
|
353
353
|
|
|
354
354
|
/** Default per-argument-value character cap inside serialized tool calls
|
|
355
355
|
* (write/edit bodies otherwise dump whole files into the archive). */
|
|
356
|
-
export const
|
|
356
|
+
export const TOOL_ARG_MAX_CHARS = 500;
|
|
357
357
|
|
|
358
358
|
/** Default character cap across one tool call's full serialized argument list. */
|
|
359
|
-
export const
|
|
359
|
+
export const TOOL_CALL_MAX_CHARS = 2000;
|
|
360
360
|
|
|
361
361
|
/** Default fraction of a truncation budget spent on the head; the remainder
|
|
362
362
|
* keeps the tail, where command errors and test failures usually land. */
|
|
363
|
-
export const
|
|
363
|
+
export const TRUNCATE_HEAD_RATIO = 0.6;
|
|
364
364
|
|
|
365
365
|
/** Zero-width ink toggles understood by the native renderer (shift-out/in):
|
|
366
366
|
* text between them prints in dim gray ink without occupying a cell. */
|
|
367
|
-
export const
|
|
368
|
-
export const
|
|
367
|
+
export const DIM_ON = "\u000e";
|
|
368
|
+
export const DIM_OFF = "\u000f";
|
|
369
369
|
|
|
370
370
|
/** Character budgets applied while serializing discarded history for frame
|
|
371
371
|
* rendering. Pass `Infinity` to disable an individual cap. */
|
|
372
|
-
export interface
|
|
373
|
-
/** Per-tool-result cap. Defaults to {@link
|
|
372
|
+
export interface SerializeOptions {
|
|
373
|
+
/** Per-tool-result cap. Defaults to {@link TOOL_RESULT_MAX_CHARS}. */
|
|
374
374
|
toolResultMaxChars?: number;
|
|
375
|
-
/** Per-argument-value cap. Defaults to {@link
|
|
375
|
+
/** Per-argument-value cap. Defaults to {@link TOOL_ARG_MAX_CHARS}. */
|
|
376
376
|
toolArgMaxChars?: number;
|
|
377
|
-
/** Whole-argument-list cap per call. Defaults to {@link
|
|
377
|
+
/** Whole-argument-list cap per call. Defaults to {@link TOOL_CALL_MAX_CHARS}. */
|
|
378
378
|
toolCallMaxChars?: number;
|
|
379
|
-
/** Head share of each budget, clamped to [0, 1]. Defaults to {@link
|
|
379
|
+
/** Head share of each budget, clamped to [0, 1]. Defaults to {@link TRUNCATE_HEAD_RATIO}. */
|
|
380
380
|
truncateHeadRatio?: number;
|
|
381
381
|
/** Print tool-result text in dim gray ink so archived conversation reads
|
|
382
382
|
* louder than archived tool noise. Defaults to `true`. */
|
|
@@ -401,11 +401,11 @@ function stripDimMarkers(text: string): string {
|
|
|
401
401
|
return text.replace(DIM_MARKERS, "");
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
-
export function
|
|
405
|
-
const toolResultMaxChars = options?.toolResultMaxChars ??
|
|
406
|
-
const toolArgMaxChars = options?.toolArgMaxChars ??
|
|
407
|
-
const toolCallMaxChars = options?.toolCallMaxChars ??
|
|
408
|
-
const headRatio = options?.truncateHeadRatio ??
|
|
404
|
+
export function serializeConversation(messages: Message[], options?: SerializeOptions): string {
|
|
405
|
+
const toolResultMaxChars = options?.toolResultMaxChars ?? TOOL_RESULT_MAX_CHARS;
|
|
406
|
+
const toolArgMaxChars = options?.toolArgMaxChars ?? TOOL_ARG_MAX_CHARS;
|
|
407
|
+
const toolCallMaxChars = options?.toolCallMaxChars ?? TOOL_CALL_MAX_CHARS;
|
|
408
|
+
const headRatio = options?.truncateHeadRatio ?? TRUNCATE_HEAD_RATIO;
|
|
409
409
|
const dimToolResults = options?.dimToolResults !== false;
|
|
410
410
|
const parts: string[] = [];
|
|
411
411
|
|
|
@@ -462,11 +462,7 @@ export function serializeSnapcompactConversation(messages: Message[], options?:
|
|
|
462
462
|
if (content) {
|
|
463
463
|
// Args above are JSON-escaped, so only raw result text can carry toggles.
|
|
464
464
|
const body = truncateForSummary(stripDimMarkers(content), toolResultMaxChars, headRatio);
|
|
465
|
-
parts.push(
|
|
466
|
-
dimToolResults
|
|
467
|
-
? `[Tool result]: ${SNAPCOMPACT_DIM_ON}${body}${SNAPCOMPACT_DIM_OFF}`
|
|
468
|
-
: `[Tool result]: ${body}`,
|
|
469
|
-
);
|
|
465
|
+
parts.push(dimToolResults ? `[Tool result]: ${DIM_ON}${body}${DIM_OFF}` : `[Tool result]: ${body}`);
|
|
470
466
|
}
|
|
471
467
|
}
|
|
472
468
|
}
|
|
@@ -527,7 +523,7 @@ const CHAR_FOLD: Record<string, string> = {
|
|
|
527
523
|
* then fold everything outside the fonts' ASCII + Latin-1 coverage to ASCII
|
|
528
524
|
* approximations (`?` as the last resort).
|
|
529
525
|
*/
|
|
530
|
-
export function
|
|
526
|
+
export function normalize(text: string): string {
|
|
531
527
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
532
528
|
let out = "";
|
|
533
529
|
for (const ch of collapsed) {
|
|
@@ -553,19 +549,15 @@ export function normalizeForSnapcompact(text: string): string {
|
|
|
553
549
|
// Rendering
|
|
554
550
|
// ============================================================================
|
|
555
551
|
|
|
556
|
-
export function
|
|
552
|
+
export function geometry(shape: Shape, size: number = shape.frameSize): Geometry {
|
|
557
553
|
const cols = Math.floor(size / shape.cellWidth);
|
|
558
554
|
const rows = Math.floor(size / shape.cellHeight / shape.lineRepeat);
|
|
559
555
|
return { cols, rows, capacity: cols * rows };
|
|
560
556
|
}
|
|
561
557
|
|
|
562
558
|
/** Render one snapcompact frame from already-normalized text. */
|
|
563
|
-
export function
|
|
564
|
-
|
|
565
|
-
shape: SnapcompactShape,
|
|
566
|
-
size: number = shape.frameSize,
|
|
567
|
-
): RenderedFrame {
|
|
568
|
-
const { cols, rows, capacity } = snapcompactGeometry(shape, size);
|
|
559
|
+
export function render(text: string, shape: Shape, size: number = shape.frameSize): RenderedFrame {
|
|
560
|
+
const { cols, rows, capacity } = geometry(shape, size);
|
|
569
561
|
const visible = text.length - (text.match(DIM_MARKERS)?.length ?? 0);
|
|
570
562
|
const chars = Math.min(visible, capacity);
|
|
571
563
|
const data = renderSnapcompactPng(text, {
|
|
@@ -579,17 +571,58 @@ export function renderSnapcompactFrame(
|
|
|
579
571
|
return { data, cols, rows, chars };
|
|
580
572
|
}
|
|
581
573
|
|
|
574
|
+
/** Options for {@link renderMany} and {@link frames}. */
|
|
575
|
+
export interface RenderManyOptions {
|
|
576
|
+
/** Explicit shape; wins over `model`. */
|
|
577
|
+
shape?: Shape;
|
|
578
|
+
/** Model whose `api` selects the eval-optimal shape. */
|
|
579
|
+
model?: Pick<Model, "api">;
|
|
580
|
+
/** Frame edge in px; defaults to the shape's `frameSize`. */
|
|
581
|
+
frameSize?: number;
|
|
582
|
+
/** Hard cap on frames produced; omit for unbounded (caller decides usage). */
|
|
583
|
+
maxFrames?: number;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Render arbitrary text into snapcompact PNG frames as LLM image blocks
|
|
588
|
+
* (first page first). Synchronous: safe to call from per-request transforms.
|
|
589
|
+
* Empty/whitespace-only input yields no frames.
|
|
590
|
+
*/
|
|
591
|
+
export function renderMany(text: string, options?: RenderManyOptions): ImageContent[] {
|
|
592
|
+
const shape = options?.shape ?? resolveShape(options?.model?.api);
|
|
593
|
+
const frameSize = options?.frameSize ?? shape.frameSize;
|
|
594
|
+
const geo = geometry(shape, frameSize);
|
|
595
|
+
const normalized = normalize(text);
|
|
596
|
+
const frames: ImageContent[] = [];
|
|
597
|
+
for (let offset = 0; offset < normalized.length; offset += geo.capacity) {
|
|
598
|
+
if (options?.maxFrames !== undefined && frames.length >= options.maxFrames) break;
|
|
599
|
+
const rendered = render(normalized.slice(offset, offset + geo.capacity), shape, frameSize);
|
|
600
|
+
frames.push({
|
|
601
|
+
type: "image",
|
|
602
|
+
data: rendered.data,
|
|
603
|
+
mimeType: "image/png",
|
|
604
|
+
...(shape.imageDetail ? { detail: shape.imageDetail } : {}),
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return frames;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Frames needed to hold `text` at the given shape/size, without rendering. */
|
|
611
|
+
export function frames(text: string, options?: Pick<RenderManyOptions, "shape" | "model" | "frameSize">): number {
|
|
612
|
+
const shape = options?.shape ?? resolveShape(options?.model?.api);
|
|
613
|
+
const geo = geometry(shape, options?.frameSize ?? shape.frameSize);
|
|
614
|
+
return Math.ceil(normalize(text).length / geo.capacity);
|
|
615
|
+
}
|
|
616
|
+
|
|
582
617
|
// ============================================================================
|
|
583
618
|
// Archive helpers
|
|
584
619
|
// ============================================================================
|
|
585
620
|
|
|
586
621
|
/** Validate and extract a persisted frame archive from `preserveData`. */
|
|
587
|
-
export function
|
|
588
|
-
|
|
589
|
-
): SnapcompactArchive | undefined {
|
|
590
|
-
const candidate = preserveData?.[SNAPCOMPACT_PRESERVE_KEY];
|
|
622
|
+
export function getPreservedArchive(preserveData: Record<string, unknown> | undefined): Archive | undefined {
|
|
623
|
+
const candidate = preserveData?.[PRESERVE_KEY];
|
|
591
624
|
if (!candidate || typeof candidate !== "object") return undefined;
|
|
592
|
-
const archive = candidate as
|
|
625
|
+
const archive = candidate as Archive;
|
|
593
626
|
if (!Array.isArray(archive.frames)) return undefined;
|
|
594
627
|
const frames = archive.frames.filter(
|
|
595
628
|
frame =>
|
|
@@ -610,7 +643,7 @@ export function getPreservedSnapcompactArchive(
|
|
|
610
643
|
}
|
|
611
644
|
|
|
612
645
|
/** Convert archive frames into LLM image blocks (oldest first). */
|
|
613
|
-
export function
|
|
646
|
+
export function images(archive: Archive): ImageContent[] {
|
|
614
647
|
return archive.frames.map(frame => ({
|
|
615
648
|
type: "image",
|
|
616
649
|
data: frame.data,
|
|
@@ -638,40 +671,40 @@ export function snapcompactImages(archive: SnapcompactArchive): ImageContent[] {
|
|
|
638
671
|
* If the previous compaction was text-based, its summary is printed at the
|
|
639
672
|
* head of the frame archive as `[Summary of earlier history]` so no continuity is lost.
|
|
640
673
|
*/
|
|
641
|
-
export async function
|
|
642
|
-
preparation:
|
|
643
|
-
options?:
|
|
644
|
-
): Promise<
|
|
674
|
+
export async function compact<TMessage = Message>(
|
|
675
|
+
preparation: CompactionPreparation<TMessage>,
|
|
676
|
+
options?: Options<TMessage>,
|
|
677
|
+
): Promise<CompactionResult> {
|
|
645
678
|
const { firstKeptEntryId, tokensBefore, previousSummary, previousPreserveData, fileOps } = preparation;
|
|
646
679
|
if (!firstKeptEntryId) {
|
|
647
680
|
throw new Error("First kept entry has no ID - session may need migration");
|
|
648
681
|
}
|
|
649
|
-
const shape = options?.shape ??
|
|
682
|
+
const shape = options?.shape ?? resolveShape(options?.model?.api);
|
|
650
683
|
const frameSize = options?.frameSize ?? shape.frameSize;
|
|
651
|
-
const maxFrames = Math.max(1, options?.maxFrames ??
|
|
652
|
-
const
|
|
684
|
+
const maxFrames = Math.max(1, options?.maxFrames ?? MAX_FRAMES);
|
|
685
|
+
const geo = geometry(shape, frameSize);
|
|
653
686
|
|
|
654
687
|
const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
|
|
655
688
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(messages);
|
|
656
|
-
let archiveText =
|
|
689
|
+
let archiveText = normalize(serializeConversation(llmMessages, options));
|
|
657
690
|
|
|
658
|
-
const previousArchive =
|
|
691
|
+
const previousArchive = getPreservedArchive(previousPreserveData);
|
|
659
692
|
const includedPreviousSummary = !previousArchive && !!previousSummary;
|
|
660
693
|
if (includedPreviousSummary && previousSummary) {
|
|
661
|
-
const head = `[Summary of earlier history] ${
|
|
694
|
+
const head = `[Summary of earlier history] ${normalize(previousSummary)}`;
|
|
662
695
|
archiveText = archiveText.length > 0 ? `${head} [Recent conversation] ${archiveText}` : head;
|
|
663
696
|
}
|
|
664
697
|
|
|
665
698
|
let truncatedChars = previousArchive?.truncatedChars ?? 0;
|
|
666
699
|
|
|
667
|
-
const newFrames:
|
|
700
|
+
const newFrames: Frame[] = [];
|
|
668
701
|
let dimOpen = false;
|
|
669
|
-
for (let offset = 0; offset < archiveText.length; offset +=
|
|
670
|
-
let chunk = archiveText.slice(offset, offset +
|
|
702
|
+
for (let offset = 0; offset < archiveText.length; offset += geo.capacity) {
|
|
703
|
+
let chunk = archiveText.slice(offset, offset + geo.capacity);
|
|
671
704
|
// Re-open a dim span that the previous frame boundary cut through.
|
|
672
|
-
if (dimOpen) chunk =
|
|
673
|
-
dimOpen = chunk.lastIndexOf(
|
|
674
|
-
const rendered =
|
|
705
|
+
if (dimOpen) chunk = DIM_ON + chunk;
|
|
706
|
+
dimOpen = chunk.lastIndexOf(DIM_ON) > chunk.lastIndexOf(DIM_OFF);
|
|
707
|
+
const rendered = render(chunk, shape, frameSize);
|
|
675
708
|
newFrames.push({
|
|
676
709
|
data: rendered.data,
|
|
677
710
|
mimeType: "image/png",
|
|
@@ -702,8 +735,8 @@ export async function snapcompactCompact<TMessage = Message>(
|
|
|
702
735
|
const totalChars = frames.reduce((sum, frame) => sum + frame.chars, 0);
|
|
703
736
|
const mixedShapes = frames.some(
|
|
704
737
|
frame =>
|
|
705
|
-
frame.cols !==
|
|
706
|
-
frame.rows !==
|
|
738
|
+
frame.cols !== geo.cols ||
|
|
739
|
+
frame.rows !== geo.rows ||
|
|
707
740
|
(frame.variant ?? "sent") !== shape.variant ||
|
|
708
741
|
(frame.lineRepeat ?? 1) !== shape.lineRepeat,
|
|
709
742
|
);
|
|
@@ -716,8 +749,8 @@ export async function snapcompactCompact<TMessage = Message>(
|
|
|
716
749
|
frameCount: frames.length,
|
|
717
750
|
multipleFrames: frames.length > 1,
|
|
718
751
|
fontCell: `${shape.cellWidth}x${shape.cellHeight}`,
|
|
719
|
-
cols:
|
|
720
|
-
rows:
|
|
752
|
+
cols: geo.cols,
|
|
753
|
+
rows: geo.rows,
|
|
721
754
|
sentenceInk: shape.variant === "sent",
|
|
722
755
|
lineRepeated: shape.lineRepeat > 1,
|
|
723
756
|
dimmedToolResults: options?.dimToolResults !== false,
|
|
@@ -727,13 +760,13 @@ export async function snapcompactCompact<TMessage = Message>(
|
|
|
727
760
|
includedPreviousSummary,
|
|
728
761
|
});
|
|
729
762
|
}
|
|
730
|
-
const { readFiles, modifiedFiles } =
|
|
731
|
-
summary =
|
|
763
|
+
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
|
764
|
+
summary = upsertFileOperations(summary, readFiles, modifiedFiles, fileOps.read);
|
|
732
765
|
|
|
733
766
|
// A snapcompact pass replaces any provider-side replacement history; strip the
|
|
734
767
|
// OpenAI remote-compaction payload like the default summarizer path does.
|
|
735
768
|
const basePreserve = stripOpenAiRemoteCompactionPreserveData(previousPreserveData) ?? {};
|
|
736
|
-
const archive:
|
|
769
|
+
const archive: Archive = { frames, totalChars, truncatedChars };
|
|
737
770
|
|
|
738
771
|
return {
|
|
739
772
|
summary,
|
|
@@ -741,6 +774,6 @@ export async function snapcompactCompact<TMessage = Message>(
|
|
|
741
774
|
firstKeptEntryId,
|
|
742
775
|
tokensBefore,
|
|
743
776
|
details: { readFiles, modifiedFiles },
|
|
744
|
-
preserveData: { ...basePreserve, [
|
|
777
|
+
preserveData: { ...basePreserve, [PRESERVE_KEY]: archive },
|
|
745
778
|
};
|
|
746
779
|
}
|