@oh-my-pi/snapcompact 16.2.7 → 16.2.9
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 +6 -0
- package/dist/types/snapcompact.d.ts +21 -1
- package/package.json +5 -5
- package/src/snapcompact.ts +93 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.2.8] - 2026-06-30
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed large snapcompact archives being reconstructed into unbounded per-request image payloads by adding a frame base64 byte budget and omitting over-budget archive frames from prompt blocks. ([#3792](https://github.com/can1357/oh-my-pi/issues/3792))
|
|
10
|
+
|
|
5
11
|
## [16.2.7] - 2026-06-30
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -317,6 +317,21 @@ export declare const HQ_EDGE_FRAMES = 3;
|
|
|
317
317
|
* cap, billed at +5% margin (ceil(4784 * 1.05)). Keeps the overflow guard from
|
|
318
318
|
* undercounting a high-res archive at the raised {@link MAX_FRAMES_DEFAULT}. */
|
|
319
319
|
export declare const FRAME_TOKEN_ESTIMATE = 5024;
|
|
320
|
+
/** Conservative upper bound for one persisted frame's base64 payload. The
|
|
321
|
+
* measured high-res Anthropic `8x13`/`11on16` PNG frames sit around 159 KB;
|
|
322
|
+
* 170 KB leaves margin for denser glyph pages without permitting multi-MB
|
|
323
|
+
* standing request bodies at large context windows. */
|
|
324
|
+
export declare const FRAME_DATA_BYTES_ESTIMATE = 170000;
|
|
325
|
+
/** Maximum snapcompact image base64 carried in every rebuilt provider request.
|
|
326
|
+
* Above this, provider backends can accept the HTTP body but fail mid-stream
|
|
327
|
+
* with opaque 5xx errors. Keep this independent from visual-token budgeting:
|
|
328
|
+
* a 1M-token model can afford 70 images on paper, but not the resulting
|
|
329
|
+
* ~11 MB JSON payload on every turn. */
|
|
330
|
+
export declare const FRAME_DATA_BYTES_BUDGET = 3000000;
|
|
331
|
+
/** Frame-count cap implied by {@link FRAME_DATA_BYTES_BUDGET}. */
|
|
332
|
+
export declare function maxFramesForDataBudget(maxFrameDataBytes?: number): number;
|
|
333
|
+
/** Base64 byte length for persisted snapcompact frames. */
|
|
334
|
+
export declare function frameDataBytes(frames: readonly Pick<Frame, "data">[]): number;
|
|
320
335
|
/**
|
|
321
336
|
* Per-request image-count budgets by provider id. These cap how many images an
|
|
322
337
|
* entire request may carry (archive/system-prompt/tool-result imaging combined).
|
|
@@ -543,13 +558,18 @@ export declare function stripPreservedArchive(preserveData: Record<string, unkno
|
|
|
543
558
|
export declare function archiveSourceText(archive: Archive): string | undefined;
|
|
544
559
|
/** Build the text used to choose and preflight a font-aware snapcompact shape. */
|
|
545
560
|
export declare function renderabilityProbeText(serialized: string, previousPreserveData?: Record<string, unknown>, previousSummary?: string): string;
|
|
561
|
+
/** Options for reconstructing a persisted snapcompact archive into prompt blocks. */
|
|
562
|
+
export interface HistoryBlockOptions {
|
|
563
|
+
/** Hard cap on image base64 bytes attached to one rebuilt provider request. */
|
|
564
|
+
maxFrameDataBytes?: number;
|
|
565
|
+
}
|
|
546
566
|
/** Convert archive frames into LLM image blocks (oldest first). */
|
|
547
567
|
export declare function images(archive: Archive): ImageContent[];
|
|
548
568
|
/** Ordered archive blocks for a compaction summary message, oldest to newest:
|
|
549
569
|
* the oldest text region, the imaged middle, then the newest text region.
|
|
550
570
|
* Runtime-only; reconstructed from {@link Archive} on each context rebuild
|
|
551
571
|
* instead of persisted on the session entry. */
|
|
552
|
-
export declare function historyBlocks(archive: Archive): (TextContent | ImageContent)[];
|
|
572
|
+
export declare function historyBlocks(archive: Archive, options?: HistoryBlockOptions): (TextContent | ImageContent)[];
|
|
553
573
|
/**
|
|
554
574
|
* Run a snapcompact compaction over prepared messages. Fully local: serializes
|
|
555
575
|
* the discarded history, appends it to the accumulated archive source text, and
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/snapcompact",
|
|
4
|
-
"version": "16.2.
|
|
4
|
+
"version": "16.2.9",
|
|
5
5
|
"description": "Bitmap-frame context compression for vision-capable LLMs",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-ai": "16.2.
|
|
35
|
-
"@oh-my-pi/pi-natives": "16.2.
|
|
36
|
-
"@oh-my-pi/pi-utils": "16.2.
|
|
37
|
-
"@oh-my-pi/pi-wire": "16.2.
|
|
34
|
+
"@oh-my-pi/pi-ai": "16.2.9",
|
|
35
|
+
"@oh-my-pi/pi-natives": "16.2.9",
|
|
36
|
+
"@oh-my-pi/pi-utils": "16.2.9",
|
|
37
|
+
"@oh-my-pi/pi-wire": "16.2.9"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/bun": "^1.3.14"
|
package/src/snapcompact.ts
CHANGED
|
@@ -424,6 +424,29 @@ export const HQ_EDGE_FRAMES = 3;
|
|
|
424
424
|
* undercounting a high-res archive at the raised {@link MAX_FRAMES_DEFAULT}. */
|
|
425
425
|
export const FRAME_TOKEN_ESTIMATE = 5024;
|
|
426
426
|
|
|
427
|
+
/** Conservative upper bound for one persisted frame's base64 payload. The
|
|
428
|
+
* measured high-res Anthropic `8x13`/`11on16` PNG frames sit around 159 KB;
|
|
429
|
+
* 170 KB leaves margin for denser glyph pages without permitting multi-MB
|
|
430
|
+
* standing request bodies at large context windows. */
|
|
431
|
+
export const FRAME_DATA_BYTES_ESTIMATE = 170_000;
|
|
432
|
+
|
|
433
|
+
/** Maximum snapcompact image base64 carried in every rebuilt provider request.
|
|
434
|
+
* Above this, provider backends can accept the HTTP body but fail mid-stream
|
|
435
|
+
* with opaque 5xx errors. Keep this independent from visual-token budgeting:
|
|
436
|
+
* a 1M-token model can afford 70 images on paper, but not the resulting
|
|
437
|
+
* ~11 MB JSON payload on every turn. */
|
|
438
|
+
export const FRAME_DATA_BYTES_BUDGET = 3_000_000;
|
|
439
|
+
|
|
440
|
+
/** Frame-count cap implied by {@link FRAME_DATA_BYTES_BUDGET}. */
|
|
441
|
+
export function maxFramesForDataBudget(maxFrameDataBytes: number = FRAME_DATA_BYTES_BUDGET): number {
|
|
442
|
+
return Math.max(1, Math.floor(maxFrameDataBytes / FRAME_DATA_BYTES_ESTIMATE));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Base64 byte length for persisted snapcompact frames. */
|
|
446
|
+
export function frameDataBytes(frames: readonly Pick<Frame, "data">[]): number {
|
|
447
|
+
return frames.reduce((sum, frame) => sum + frame.data.length, 0);
|
|
448
|
+
}
|
|
449
|
+
|
|
427
450
|
/**
|
|
428
451
|
* Per-request image-count budgets by provider id. These cap how many images an
|
|
429
452
|
* entire request may carry (archive/system-prompt/tool-result imaging combined).
|
|
@@ -1542,6 +1565,54 @@ export function renderabilityProbeText(
|
|
|
1542
1565
|
return serialized;
|
|
1543
1566
|
}
|
|
1544
1567
|
|
|
1568
|
+
/** Options for reconstructing a persisted snapcompact archive into prompt blocks. */
|
|
1569
|
+
export interface HistoryBlockOptions {
|
|
1570
|
+
/** Hard cap on image base64 bytes attached to one rebuilt provider request. */
|
|
1571
|
+
maxFrameDataBytes?: number;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function formatFrameDataBytes(bytes: number): string {
|
|
1575
|
+
if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`;
|
|
1576
|
+
if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(1)} KB`;
|
|
1577
|
+
return `${bytes} B`;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function imagesWithinBudget(
|
|
1581
|
+
archive: Archive,
|
|
1582
|
+
maxFrameDataBytes: number | undefined,
|
|
1583
|
+
): { images: ImageContent[]; omittedFrames: number; omittedBytes: number } {
|
|
1584
|
+
if (maxFrameDataBytes === undefined) {
|
|
1585
|
+
return { images: images(archive), omittedFrames: 0, omittedBytes: 0 };
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
let usedBytes = 0;
|
|
1589
|
+
let omittedFrames = 0;
|
|
1590
|
+
let omittedBytes = 0;
|
|
1591
|
+
const keptNewestFirst: Frame[] = [];
|
|
1592
|
+
for (let index = archive.frames.length - 1; index >= 0; index--) {
|
|
1593
|
+
const frame = archive.frames[index];
|
|
1594
|
+
if (!frame) continue;
|
|
1595
|
+
const bytes = frame.data.length;
|
|
1596
|
+
if (usedBytes + bytes > maxFrameDataBytes) {
|
|
1597
|
+
omittedFrames++;
|
|
1598
|
+
omittedBytes += bytes;
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
usedBytes += bytes;
|
|
1602
|
+
keptNewestFirst.push(frame);
|
|
1603
|
+
}
|
|
1604
|
+
keptNewestFirst.reverse();
|
|
1605
|
+
return { images: images({ ...archive, frames: keptNewestFirst }), omittedFrames, omittedBytes };
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function omittedFrameNotice(omittedFrames: number, omittedBytes: number): string {
|
|
1609
|
+
return [
|
|
1610
|
+
"-------------- snapcompact image middle omitted",
|
|
1611
|
+
`${omittedFrames.toLocaleString()} archived image frame${omittedFrames === 1 ? "" : "s"} (${formatFrameDataBytes(omittedBytes)} base64) exceeded the per-request snapcompact payload budget. The compacted summary and visible text edges remain available.`,
|
|
1612
|
+
"--------------",
|
|
1613
|
+
].join("\n");
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1545
1616
|
/** Convert archive frames into LLM image blocks (oldest first). */
|
|
1546
1617
|
export function images(archive: Archive): ImageContent[] {
|
|
1547
1618
|
return archive.frames.map(frame => ({
|
|
@@ -1555,23 +1626,38 @@ export function images(archive: Archive): ImageContent[] {
|
|
|
1555
1626
|
* the oldest text region, the imaged middle, then the newest text region.
|
|
1556
1627
|
* Runtime-only; reconstructed from {@link Archive} on each context rebuild
|
|
1557
1628
|
* instead of persisted on the session entry. */
|
|
1558
|
-
export function historyBlocks(archive: Archive): (TextContent | ImageContent)[] {
|
|
1629
|
+
export function historyBlocks(archive: Archive, options: HistoryBlockOptions = {}): (TextContent | ImageContent)[] {
|
|
1559
1630
|
const blocks: (TextContent | ImageContent)[] = [];
|
|
1560
|
-
const
|
|
1631
|
+
const budgeted = imagesWithinBudget(archive, options.maxFrameDataBytes);
|
|
1632
|
+
const hasImages = budgeted.images.length > 0;
|
|
1633
|
+
const hasOmittedImages = budgeted.omittedFrames > 0;
|
|
1561
1634
|
if (archive.textHead) {
|
|
1562
|
-
const suffix = hasImages
|
|
1635
|
+
const suffix = hasImages
|
|
1636
|
+
? "\n-------------- imaged middle below\n"
|
|
1637
|
+
: hasOmittedImages
|
|
1638
|
+
? `\n${omittedFrameNotice(budgeted.omittedFrames, budgeted.omittedBytes)}\n`
|
|
1639
|
+
: "";
|
|
1563
1640
|
blocks.push({ type: "text", text: toPlainText(archive.textHead) + suffix });
|
|
1641
|
+
} else if (hasOmittedImages && !hasImages) {
|
|
1642
|
+
blocks.push({ type: "text", text: omittedFrameNotice(budgeted.omittedFrames, budgeted.omittedBytes) });
|
|
1643
|
+
}
|
|
1644
|
+
// Omitted frames are the OLDEST archived images: the byte budget keeps the
|
|
1645
|
+
// newest tail frames, so the gap notice precedes the kept images to keep the
|
|
1646
|
+
// reconstructed blocks oldest-to-newest.
|
|
1647
|
+
if (hasImages && hasOmittedImages) {
|
|
1648
|
+
blocks.push({ type: "text", text: omittedFrameNotice(budgeted.omittedFrames, budgeted.omittedBytes) });
|
|
1564
1649
|
}
|
|
1565
|
-
blocks.push(...images
|
|
1650
|
+
blocks.push(...budgeted.images);
|
|
1566
1651
|
if (archive.textTail) {
|
|
1567
1652
|
const prefix = hasImages
|
|
1568
1653
|
? "-------------- imaged middle above\n"
|
|
1569
|
-
: archive.truncatedChars > 0
|
|
1654
|
+
: archive.truncatedChars > 0 || hasOmittedImages
|
|
1570
1655
|
? "\n-------------- middle history omitted above\n"
|
|
1571
1656
|
: "";
|
|
1572
1657
|
const tail = prefix + toPlainText(archive.textTail);
|
|
1573
|
-
|
|
1574
|
-
|
|
1658
|
+
const lastBlock = blocks[blocks.length - 1];
|
|
1659
|
+
if (lastBlock?.type === "text") {
|
|
1660
|
+
lastBlock.text += tail;
|
|
1575
1661
|
} else {
|
|
1576
1662
|
blocks.push({ type: "text", text: tail });
|
|
1577
1663
|
}
|