@oh-my-pi/snapcompact 17.4.0 → 17.4.2
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/LICENSE +22 -0
- package/THIRD-PARTY-NOTICES.txt +22909 -0
- package/dist/types/snapcompact.d.ts +2 -0
- package/package.json +9 -7
- package/src/snapcompact.ts +149 -9
|
@@ -344,6 +344,8 @@ export declare const PROVIDER_IMAGE_BUDGETS: Record<string, number>;
|
|
|
344
344
|
export declare const DEFAULT_PROVIDER_IMAGE_BUDGET = 5;
|
|
345
345
|
/** Per-request image budget for `provider`; unknown providers get the floor. */
|
|
346
346
|
export declare function providerImageBudget(provider: string | undefined): number;
|
|
347
|
+
/** Archive frame cap for `provider`: image budget, never above {@link MAX_FRAMES_DEFAULT}. */
|
|
348
|
+
export declare function providerFrameBudget(provider: string | undefined): number;
|
|
347
349
|
/** Key under `CompactionEntry.preserveData` holding the frame archive. */
|
|
348
350
|
export declare const PRESERVE_KEY = "snapcompact";
|
|
349
351
|
/** One developed snapcompact frame: a base64 PNG plus its reading geometry. */
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/snapcompact",
|
|
4
|
-
"version": "17.4.
|
|
4
|
+
"version": "17.4.2",
|
|
5
5
|
"description": "Bitmap-frame context compression for vision-capable LLMs",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
|
-
"author": "
|
|
7
|
+
"author": "Stencil Labs, Inc.",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-ai": "17.4.
|
|
35
|
-
"@oh-my-pi/pi-catalog": "17.4.
|
|
36
|
-
"@oh-my-pi/pi-natives": "17.4.
|
|
37
|
-
"@oh-my-pi/pi-utils": "17.4.
|
|
38
|
-
"@oh-my-pi/pi-wire": "17.4.
|
|
34
|
+
"@oh-my-pi/pi-ai": "17.4.2",
|
|
35
|
+
"@oh-my-pi/pi-catalog": "17.4.2",
|
|
36
|
+
"@oh-my-pi/pi-natives": "17.4.2",
|
|
37
|
+
"@oh-my-pi/pi-utils": "17.4.2",
|
|
38
|
+
"@oh-my-pi/pi-wire": "17.4.2"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/bun": "^1.3.14"
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
"src",
|
|
48
48
|
"README.md",
|
|
49
49
|
"CHANGELOG.md",
|
|
50
|
+
"LICENSE",
|
|
51
|
+
"THIRD-PARTY-NOTICES.txt",
|
|
50
52
|
"dist/types"
|
|
51
53
|
],
|
|
52
54
|
"exports": {
|
package/src/snapcompact.ts
CHANGED
|
@@ -519,6 +519,11 @@ export function providerImageBudget(provider: string | undefined): number {
|
|
|
519
519
|
return (provider !== undefined ? PROVIDER_IMAGE_BUDGETS[provider] : undefined) ?? DEFAULT_PROVIDER_IMAGE_BUDGET;
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
+
/** Archive frame cap for `provider`: image budget, never above {@link MAX_FRAMES_DEFAULT}. */
|
|
523
|
+
export function providerFrameBudget(provider: string | undefined): number {
|
|
524
|
+
return Math.min(providerImageBudget(provider), MAX_FRAMES_DEFAULT);
|
|
525
|
+
}
|
|
526
|
+
|
|
522
527
|
/** Key under `CompactionEntry.preserveData` holding the frame archive. */
|
|
523
528
|
export const PRESERVE_KEY = "snapcompact";
|
|
524
529
|
|
|
@@ -775,6 +780,132 @@ function truncateForSummary(text: string, maxChars: number, headRatio: number):
|
|
|
775
780
|
return `${text.slice(0, headChars)} […${elided}ch elided…] ${tail}`;
|
|
776
781
|
}
|
|
777
782
|
|
|
783
|
+
/** One elision marker as emitted by {@link truncateForSummary} (Unicode
|
|
784
|
+
* ellipses) or as persisted after `normalize()` (ASCII dots). */
|
|
785
|
+
const ELIDED_MARKER = String.raw`\[(?:…|\.{3})\d+ch elided(?:…|\.{3})\]`;
|
|
786
|
+
|
|
787
|
+
/** Unquoted RFC 2045 token used as a media-type parameter name or value.
|
|
788
|
+
* Quoted-string values (RFC 822) are out of scope. */
|
|
789
|
+
const MEDIA_TYPE_TOKEN = String.raw`[\w!#$%&'*+.^|~-]+`;
|
|
790
|
+
|
|
791
|
+
/** An inline base64 data URL. The payload may be empty or carry one embedded
|
|
792
|
+
* elision marker so fragments left by pre-guard slices — including a cut
|
|
793
|
+
* landing exactly on `;base64,` — still match. RFC 2397 allows `*( ";" parameter )`
|
|
794
|
+
* between type/subtype and the terminal `;base64`; unquoted tokens are matched,
|
|
795
|
+
* quoted-string values are out of scope. `data:` and `base64` match
|
|
796
|
+
* case-insensitively (`gi`). Matching starts at `data:`; Markdown wrappers are
|
|
797
|
+
* recovered by {@link adjacentMarkdownOpenerStart} after each hit. */
|
|
798
|
+
const DATA_URL_ATOM = new RegExp(
|
|
799
|
+
String.raw`data:([A-Za-z][\w.+-]*\/[\w.+-]+(?:;${MEDIA_TYPE_TOKEN}=${MEDIA_TYPE_TOKEN})*);base64,` +
|
|
800
|
+
String.raw`([A-Za-z0-9+/=]*(?:\s*${ELIDED_MARKER}\s*[A-Za-z0-9+/=]*)?)` +
|
|
801
|
+
String.raw`(\s*\))?`,
|
|
802
|
+
"gi",
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
const ELIDED_MARKER_RE = new RegExp(String.raw`\s*${ELIDED_MARKER}\s*`);
|
|
806
|
+
const MARKDOWN_WHITESPACE_CHAR = /\s/;
|
|
807
|
+
|
|
808
|
+
/** Canonical base64: 4-char groups with valid terminal padding. */
|
|
809
|
+
const CANONICAL_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/;
|
|
810
|
+
|
|
811
|
+
/** A non-canonical payload at least this long is a damaged fragment of a real
|
|
812
|
+
* data URL (e.g. an archive head cut mid-payload by a structure-blind slice),
|
|
813
|
+
* not a prose mention like `data:image/png;base64,abc`. */
|
|
814
|
+
const DAMAGED_PAYLOAD_MIN_CHARS = 40;
|
|
815
|
+
|
|
816
|
+
/** Context for {@link elideDataUrls}. `source` text is intact (never sliced),
|
|
817
|
+
* so a short non-canonical payload is a prose mention and stays untouched.
|
|
818
|
+
* `archive` text may have been cut by pre-guard structure-blind slices at
|
|
819
|
+
* any offset — even 0–39 chars past `;base64,` — so every recognized prefix
|
|
820
|
+
* is suspect and is always elided. */
|
|
821
|
+
type DataUrlContext = "source" | "archive";
|
|
822
|
+
|
|
823
|
+
/** Start of `!?[label](\s*` immediately before `dataIndex`, or `undefined`.
|
|
824
|
+
* The opener must lie in `[cursor, dataIndex)`. Nested `[` in the label is
|
|
825
|
+
* kept (the old `[^\]\n]*` class allowed it) by taking the earliest `[` after
|
|
826
|
+
* a prior `]`, newline, or `cursor`; the scan never walks already-emitted
|
|
827
|
+
* text, so repeated `](data:...)` stays linear. */
|
|
828
|
+
function adjacentMarkdownOpenerStart(text: string, dataIndex: number, cursor: number): number | undefined {
|
|
829
|
+
let i = dataIndex;
|
|
830
|
+
while (i > cursor && MARKDOWN_WHITESPACE_CHAR.test(text.charAt(i - 1))) i--;
|
|
831
|
+
// `](` and any following whitespace must sit in [cursor, dataIndex).
|
|
832
|
+
if (i - 2 < cursor || text.charAt(i - 1) !== "(" || text.charAt(i - 2) !== "]") return undefined;
|
|
833
|
+
let opener = -1;
|
|
834
|
+
for (let j = i - 3; j >= cursor; j--) {
|
|
835
|
+
const c = text.charAt(j);
|
|
836
|
+
if (c === "]" || c === "\n") break;
|
|
837
|
+
if (c === "[") opener = j;
|
|
838
|
+
}
|
|
839
|
+
if (opener < 0) return undefined;
|
|
840
|
+
return opener > cursor && text.charAt(opener - 1) === "!" ? opener - 1 : opener;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** Replace every inline base64 data URL atomically with a deterministic
|
|
844
|
+
* placeholder. A character cap that slices inside a base64 payload leaves a
|
|
845
|
+
* recognizable image reference that can never decode; OpenAI-dialect
|
|
846
|
+
* providers reject such requests as invalid image input, and because the
|
|
847
|
+
* corrupted text persists in the archive the session re-fails on every later
|
|
848
|
+
* request. The payload is worthless to a model as text, so the whole atom —
|
|
849
|
+
* Markdown wrapper included — collapses to its metadata. Payloads already
|
|
850
|
+
* carrying an elision marker, and non-canonical fragments left by pre-guard
|
|
851
|
+
* slices, are healed the same way.
|
|
852
|
+
*
|
|
853
|
+
* The placeholder's `<mime>` is the media type as written: type/subtype plus
|
|
854
|
+
* any unquoted RFC 2397 `;parameter=value` segments, original case preserved.
|
|
855
|
+
* Parameters are kept rather than stripped to a bare type/subtype so charset
|
|
856
|
+
* (and similar) remain visible after elision and the label stays a pure
|
|
857
|
+
* function of the captured text. */
|
|
858
|
+
function elideDataUrls(text: string, context: DataUrlContext = "source"): string {
|
|
859
|
+
if (!/;base64,/i.test(text)) return text;
|
|
860
|
+
DATA_URL_ATOM.lastIndex = 0;
|
|
861
|
+
let match = DATA_URL_ATOM.exec(text);
|
|
862
|
+
if (match === null) return text;
|
|
863
|
+
const out: string[] = [];
|
|
864
|
+
let cursor = 0;
|
|
865
|
+
while (match !== null) {
|
|
866
|
+
const urlStart = match.index;
|
|
867
|
+
const urlEnd = urlStart + match[0].length;
|
|
868
|
+
const mime = match[1] ?? "";
|
|
869
|
+
const payload = match[2] ?? "";
|
|
870
|
+
const closer = match[3];
|
|
871
|
+
const marker = ELIDED_MARKER_RE.exec(payload);
|
|
872
|
+
const isAtom =
|
|
873
|
+
context === "archive" ||
|
|
874
|
+
marker !== null ||
|
|
875
|
+
CANONICAL_BASE64.test(payload) ||
|
|
876
|
+
payload.length >= DAMAGED_PAYLOAD_MIN_CHARS;
|
|
877
|
+
if (!isAtom) {
|
|
878
|
+
// Advance through short prose too, so a later wrapper cannot swallow
|
|
879
|
+
// a data URL already copied out of its Markdown label.
|
|
880
|
+
out.push(text.slice(cursor, urlEnd));
|
|
881
|
+
cursor = urlEnd;
|
|
882
|
+
} else {
|
|
883
|
+
const b64Chars = marker
|
|
884
|
+
? payload.length - marker[0].length + Number(/\d+/.exec(marker[0])?.[0] ?? 0)
|
|
885
|
+
: payload.length;
|
|
886
|
+
const placeholder = `[data URL omitted: ${mime}, ${b64Chars} base64 chars]`;
|
|
887
|
+
const foundOpener = adjacentMarkdownOpenerStart(text, urlStart, cursor);
|
|
888
|
+
const openerStart = foundOpener !== undefined && foundOpener >= cursor ? foundOpener : undefined;
|
|
889
|
+
const emitStart = openerStart ?? urlStart;
|
|
890
|
+
out.push(text.slice(cursor, emitStart));
|
|
891
|
+
// Swallow the Markdown wrapper only when both delimiters matched;
|
|
892
|
+
// otherwise re-emit whichever half was captured untouched. An opener
|
|
893
|
+
// that starts before the already-emitted cursor would overlap a prior
|
|
894
|
+
// replacement, so that URL is treated as bare.
|
|
895
|
+
if (openerStart !== undefined && closer !== undefined) {
|
|
896
|
+
out.push(placeholder);
|
|
897
|
+
} else {
|
|
898
|
+
const opener = openerStart !== undefined ? text.slice(openerStart, urlStart) : "";
|
|
899
|
+
out.push(opener, placeholder, closer ?? "");
|
|
900
|
+
}
|
|
901
|
+
cursor = urlEnd;
|
|
902
|
+
}
|
|
903
|
+
match = DATA_URL_ATOM.exec(text);
|
|
904
|
+
}
|
|
905
|
+
out.push(text.slice(cursor));
|
|
906
|
+
return out.join("");
|
|
907
|
+
}
|
|
908
|
+
|
|
778
909
|
const DIM_MARKERS = /[\u000e\u000f]/g;
|
|
779
910
|
|
|
780
911
|
/** Plain-text history kept verbatim at each chronological edge, in HQ-frame-
|
|
@@ -836,7 +967,7 @@ export function serializeConversation(messages: Message[], options?: SerializeOp
|
|
|
836
967
|
// Wrap a raw tool-result body in an `<out>` block, dimming only the body so
|
|
837
968
|
// the frame coloring keeps scope markers and calls loud.
|
|
838
969
|
const renderResultBlock = (rawText: string): string => {
|
|
839
|
-
const body = truncateForSummary(stripDimMarkers(rawText), toolResultMaxChars, headRatio);
|
|
970
|
+
const body = truncateForSummary(elideDataUrls(stripDimMarkers(rawText)), toolResultMaxChars, headRatio);
|
|
840
971
|
return `<out>\n${dimToolResults ? `${DIM_ON}${body}${DIM_OFF}` : body}\n</out>`;
|
|
841
972
|
};
|
|
842
973
|
|
|
@@ -894,7 +1025,7 @@ export function serializeConversation(messages: Message[], options?: SerializeOp
|
|
|
894
1025
|
.filter(([key]) => key !== INTENT_FIELD)
|
|
895
1026
|
.map(
|
|
896
1027
|
([key, value]) =>
|
|
897
|
-
`${key}=${truncateForSummary(JSON.stringify(value) ?? "undefined", toolArgMaxChars, headRatio)}`,
|
|
1028
|
+
`${key}=${truncateForSummary(elideDataUrls(JSON.stringify(value) ?? "undefined"), toolArgMaxChars, headRatio)}`,
|
|
898
1029
|
)
|
|
899
1030
|
.join(", "),
|
|
900
1031
|
toolCallMaxChars,
|
|
@@ -1616,7 +1747,7 @@ export function archiveSourceText(archive: Archive): string | undefined {
|
|
|
1616
1747
|
[archive.textHead, archive.textTail]
|
|
1617
1748
|
.filter((part): part is string => typeof part === "string" && part.length > 0)
|
|
1618
1749
|
.join(NEWLINE_GLYPH);
|
|
1619
|
-
return text.length > 0 ? toPlainText(text) : undefined;
|
|
1750
|
+
return text.length > 0 ? elideDataUrls(toPlainText(text), "archive") : undefined;
|
|
1620
1751
|
}
|
|
1621
1752
|
|
|
1622
1753
|
/** Build the text used to choose and preflight a font-aware snapcompact shape. */
|
|
@@ -1704,7 +1835,7 @@ export function historyBlocks(archive: Archive, options: HistoryBlockOptions = {
|
|
|
1704
1835
|
: hasOmittedImages
|
|
1705
1836
|
? `\n${omittedFrameNotice(budgeted.omittedFrames, budgeted.omittedBytes)}\n`
|
|
1706
1837
|
: "";
|
|
1707
|
-
blocks.push({ type: "text", text: toPlainText(archive.textHead) + suffix });
|
|
1838
|
+
blocks.push({ type: "text", text: elideDataUrls(toPlainText(archive.textHead), "archive") + suffix });
|
|
1708
1839
|
} else if (hasOmittedImages && !hasImages) {
|
|
1709
1840
|
blocks.push({ type: "text", text: omittedFrameNotice(budgeted.omittedFrames, budgeted.omittedBytes) });
|
|
1710
1841
|
}
|
|
@@ -1721,7 +1852,7 @@ export function historyBlocks(archive: Archive, options: HistoryBlockOptions = {
|
|
|
1721
1852
|
: archive.truncatedChars > 0 || hasOmittedImages
|
|
1722
1853
|
? "\n-------------- middle history omitted above\n"
|
|
1723
1854
|
: "";
|
|
1724
|
-
const tail = prefix + toPlainText(archive.textTail);
|
|
1855
|
+
const tail = prefix + elideDataUrls(toPlainText(archive.textTail), "archive");
|
|
1725
1856
|
const lastBlock = blocks[blocks.length - 1];
|
|
1726
1857
|
if (lastBlock?.type === "text") {
|
|
1727
1858
|
lastBlock.text += tail;
|
|
@@ -1917,11 +2048,14 @@ export async function compact<TMessage = Message>(
|
|
|
1917
2048
|
.join(NEWLINE_GLYPH);
|
|
1918
2049
|
// Legacy archives may carry `¶think:` sections from before includeThinking
|
|
1919
2050
|
// existed; scrub them when this compaction excludes thinking so the
|
|
1920
|
-
// re-rendered archive stops replaying reasoning (issue #6093).
|
|
2051
|
+
// re-rendered archive stops replaying reasoning (issue #6093). They may
|
|
2052
|
+
// also carry data URLs a pre-guard slice cut at any offset; heal those in
|
|
2053
|
+
// archive context before the text is folded into the new source.
|
|
2054
|
+
const previousTextHealed = elideDataUrls(previousTextRaw, "archive");
|
|
1921
2055
|
const previousText =
|
|
1922
|
-
options?.includeThinking === false &&
|
|
1923
|
-
? stripThinkingSections(
|
|
1924
|
-
:
|
|
2056
|
+
options?.includeThinking === false && previousTextHealed.length > 0
|
|
2057
|
+
? stripThinkingSections(previousTextHealed)
|
|
2058
|
+
: previousTextHealed;
|
|
1925
2059
|
const hasPreviousText = previousText.length > 0;
|
|
1926
2060
|
const includedPreviousSummary = !hasPreviousText && !!previousSummary;
|
|
1927
2061
|
const shapeProbeText = renderabilityProbeText(serialized, previousPreserveData, previousSummary);
|
|
@@ -1949,6 +2083,12 @@ export async function compact<TMessage = Message>(
|
|
|
1949
2083
|
if (hasPreviousText) {
|
|
1950
2084
|
archiveText = archiveText.length > 0 ? `${previousText}${NEWLINE_GLYPH}${archiveText}` : previousText;
|
|
1951
2085
|
}
|
|
2086
|
+
// Data URLs must never reach planArchive: its edge slices are structure-
|
|
2087
|
+
// blind, and a split payload replays as broken image input on every later
|
|
2088
|
+
// request. previousText is already strictly healed above; this source-mode
|
|
2089
|
+
// pass covers intact URLs in fresh user/assistant text, which the
|
|
2090
|
+
// serializer never truncates.
|
|
2091
|
+
archiveText = elideDataUrls(archiveText);
|
|
1952
2092
|
|
|
1953
2093
|
const layout = planArchive(archiveText, high, low, maxFrames);
|
|
1954
2094
|
truncatedChars += layout.truncatedChars;
|