@oh-my-pi/snapcompact 18.2.0 → 18.2.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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.2.1] - 2026-09-15
6
+
7
+ ### Changed
8
+
9
+ - `historyBlocks()` now resolves persisted frame payloads lazily, keeps the newest frames within a byte budget, and drops unresolved blob references instead of sending them to providers ([#10227](https://github.com/can1357/oh-my-pi/pull/10227) by [@lemonleks](https://github.com/lemonleks)).
10
+
5
11
  ## [18.1.18] - 2026-09-11
6
12
 
7
13
  ### Fixed
@@ -566,10 +566,20 @@ export declare function stripPreservedArchive(preserveData: Record<string, unkno
566
566
  export declare function archiveSourceText(archive: Archive): string | undefined;
567
567
  /** Build the text used to choose and preflight a font-aware snapcompact shape. */
568
568
  export declare function renderabilityProbeText(serialized: string, previousPreserveData?: Record<string, unknown>, previousSummary?: string): string;
569
+ /** A frame payload that can be priced before it is materialized. */
570
+ export interface LazyFrameData {
571
+ readonly bytes: number;
572
+ read(): string;
573
+ }
569
574
  /** Options for reconstructing a persisted snapcompact archive into prompt blocks. */
570
575
  export interface HistoryBlockOptions {
571
576
  /** Hard cap on image base64 bytes attached to one rebuilt provider request. */
572
577
  maxFrameDataBytes?: number;
578
+ /**
579
+ * Price and resolve a frame payload. Called in newest-first budget order.
580
+ * Returning `undefined` drops a missing payload.
581
+ */
582
+ resolveFrameData?: (data: string) => LazyFrameData | undefined;
573
583
  }
574
584
  /** Convert archive frames into LLM image blocks (oldest first). */
575
585
  export declare function images(archive: Archive): ImageContent[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/snapcompact",
4
- "version": "18.2.0",
4
+ "version": "18.2.2",
5
5
  "description": "Bitmap-frame context compression for vision-capable LLMs",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -31,11 +31,11 @@
31
31
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
32
32
  },
33
33
  "dependencies": {
34
- "@oh-my-pi/pi-ai": "18.2.0",
35
- "@oh-my-pi/pi-catalog": "18.2.0",
36
- "@oh-my-pi/pi-natives": "18.2.0",
37
- "@oh-my-pi/pi-utils": "18.2.0",
38
- "@oh-my-pi/pi-wire": "18.2.0"
34
+ "@oh-my-pi/pi-ai": "18.2.2",
35
+ "@oh-my-pi/pi-catalog": "18.2.2",
36
+ "@oh-my-pi/pi-natives": "18.2.2",
37
+ "@oh-my-pi/pi-utils": "18.2.2",
38
+ "@oh-my-pi/pi-wire": "18.2.2"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/bun": "^1.3.14"
@@ -1769,10 +1769,21 @@ export function renderabilityProbeText(
1769
1769
  return serialized;
1770
1770
  }
1771
1771
 
1772
+ /** A frame payload that can be priced before it is materialized. */
1773
+ export interface LazyFrameData {
1774
+ readonly bytes: number;
1775
+ read(): string;
1776
+ }
1777
+
1772
1778
  /** Options for reconstructing a persisted snapcompact archive into prompt blocks. */
1773
1779
  export interface HistoryBlockOptions {
1774
1780
  /** Hard cap on image base64 bytes attached to one rebuilt provider request. */
1775
1781
  maxFrameDataBytes?: number;
1782
+ /**
1783
+ * Price and resolve a frame payload. Called in newest-first budget order.
1784
+ * Returning `undefined` drops a missing payload.
1785
+ */
1786
+ resolveFrameData?: (data: string) => LazyFrameData | undefined;
1776
1787
  }
1777
1788
 
1778
1789
  function formatFrameDataBytes(bytes: number): string {
@@ -1781,38 +1792,105 @@ function formatFrameDataBytes(bytes: number): string {
1781
1792
  return `${bytes} B`;
1782
1793
  }
1783
1794
 
1784
- function imagesWithinBudget(
1785
- archive: Archive,
1786
- maxFrameDataBytes: number | undefined,
1787
- ): { images: ImageContent[]; omittedFrames: number; omittedBytes: number } {
1788
- if (maxFrameDataBytes === undefined) {
1789
- return { images: images(archive), omittedFrames: 0, omittedBytes: 0 };
1795
+ /**
1796
+ * Prefix of an externalized frame payload (see the session blob store).
1797
+ *
1798
+ * A frame persisted by a recent session holds this reference rather than
1799
+ * base64, so a caller that never supplies `resolveFrameData` would otherwise
1800
+ * hand the reference string to the provider as image data. Dropping the frame
1801
+ * is the safe failure: a missing picture beats a rejected request.
1802
+ */
1803
+ const BLOB_REFERENCE_PREFIX = "blob:sha256:";
1804
+
1805
+ function isUnresolvedBlobReference(data: string): boolean {
1806
+ return data.startsWith(BLOB_REFERENCE_PREFIX);
1807
+ }
1808
+
1809
+ /** One reconstructed slot: a usable frame, an unavailable gap, or a byte-budget gap. */
1810
+ type FrameSlot = { frame: Frame } | { unavailable: true } | { omittedBytes: number };
1811
+
1812
+ /**
1813
+ * Price every frame newest-first and retain only payloads that fit the byte
1814
+ * budget. Gap slots preserve the original chronology without materializing
1815
+ * rejected payloads.
1816
+ */
1817
+ function imagesWithinBudget(archive: Archive, options: HistoryBlockOptions): FrameSlot[] {
1818
+ const { maxFrameDataBytes, resolveFrameData } = options;
1819
+ const hasUnresolvedReference = archive.frames.some(frame => isUnresolvedBlobReference(frame.data));
1820
+ if (maxFrameDataBytes === undefined && !resolveFrameData && !hasUnresolvedReference) {
1821
+ return archive.frames.map(frame => ({ frame }));
1790
1822
  }
1791
1823
 
1792
1824
  let usedBytes = 0;
1793
- let omittedFrames = 0;
1794
- let omittedBytes = 0;
1795
- const keptNewestFirst: Frame[] = [];
1825
+ const newestFirst: FrameSlot[] = [];
1796
1826
  for (let index = archive.frames.length - 1; index >= 0; index--) {
1797
1827
  const frame = archive.frames[index];
1798
1828
  if (!frame) continue;
1799
- const bytes = frame.data.length;
1800
- if (usedBytes + bytes > maxFrameDataBytes) {
1801
- omittedFrames++;
1802
- omittedBytes += bytes;
1829
+ const lazy = resolveFrameData?.(frame.data);
1830
+ if (!lazy && (resolveFrameData || isUnresolvedBlobReference(frame.data))) {
1831
+ newestFirst.push({ unavailable: true });
1832
+ continue;
1833
+ }
1834
+ const bytes = lazy ? lazy.bytes : frame.data.length;
1835
+ if (maxFrameDataBytes !== undefined && usedBytes + bytes > maxFrameDataBytes) {
1836
+ newestFirst.push({ omittedBytes: bytes });
1803
1837
  continue;
1804
1838
  }
1805
1839
  usedBytes += bytes;
1806
- keptNewestFirst.push(frame);
1840
+ newestFirst.push({ frame: lazy ? { ...frame, data: lazy.read() } : frame });
1807
1841
  }
1808
- keptNewestFirst.reverse();
1809
- return { images: images({ ...archive, frames: keptNewestFirst }), omittedFrames, omittedBytes };
1842
+ newestFirst.reverse();
1843
+ return newestFirst;
1844
+ }
1845
+
1846
+ /** Collapse a run of unavailable frames into one in-place gap marker. */
1847
+ function unavailableFrameNotice(count: number): string {
1848
+ return `-------------- ${count.toLocaleString()} archived image frame${count === 1 ? "" : "s"} unavailable here --------------`;
1849
+ }
1850
+
1851
+ /** Blocks for the imaged middle, with both gap causes kept in chronological position. */
1852
+ function frameBlocks(slots: FrameSlot[]): (TextContent | ImageContent)[] {
1853
+ const blocks: (TextContent | ImageContent)[] = [];
1854
+ let pendingGap: "unavailable" | "budget" | undefined;
1855
+ let pendingFrames = 0;
1856
+ let pendingBytes = 0;
1857
+ const flushGap = (): void => {
1858
+ if (!pendingGap) return;
1859
+ blocks.push({
1860
+ type: "text",
1861
+ text:
1862
+ pendingGap === "unavailable"
1863
+ ? unavailableFrameNotice(pendingFrames)
1864
+ : omittedFrameNotice(pendingFrames, pendingBytes),
1865
+ });
1866
+ pendingGap = undefined;
1867
+ pendingFrames = 0;
1868
+ pendingBytes = 0;
1869
+ };
1870
+ for (const slot of slots) {
1871
+ if ("frame" in slot) {
1872
+ flushGap();
1873
+ blocks.push(...images({ frames: [slot.frame] } as Archive));
1874
+ continue;
1875
+ }
1876
+ const gap = "unavailable" in slot ? "unavailable" : "budget";
1877
+ if (pendingGap && pendingGap !== gap) flushGap();
1878
+ pendingGap = gap;
1879
+ pendingFrames++;
1880
+ if ("omittedBytes" in slot) pendingBytes += slot.omittedBytes;
1881
+ }
1882
+ flushGap();
1883
+ return blocks;
1810
1884
  }
1811
1885
 
1812
1886
  function omittedFrameNotice(omittedFrames: number, omittedBytes: number): string {
1887
+ const budgetNote =
1888
+ omittedBytes > 0
1889
+ ? ` ${formatFrameDataBytes(omittedBytes)} of base64 exceeded the per-request snapcompact payload budget.`
1890
+ : "";
1813
1891
  return [
1814
1892
  "-------------- snapcompact image middle omitted",
1815
- `${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.`,
1893
+ `${omittedFrames.toLocaleString()} archived image frame${omittedFrames === 1 ? "" : "s"} could not be included.${budgetNote} The compacted summary and visible text edges remain available.`,
1816
1894
  "--------------",
1817
1895
  ].join("\n");
1818
1896
  }
@@ -1832,26 +1910,14 @@ export function images(archive: Archive): ImageContent[] {
1832
1910
  * instead of persisted on the session entry. */
1833
1911
  export function historyBlocks(archive: Archive, options: HistoryBlockOptions = {}): (TextContent | ImageContent)[] {
1834
1912
  const blocks: (TextContent | ImageContent)[] = [];
1835
- const budgeted = imagesWithinBudget(archive, options.maxFrameDataBytes);
1836
- const hasImages = budgeted.images.length > 0;
1837
- const hasOmittedImages = budgeted.omittedFrames > 0;
1913
+ const middle = frameBlocks(imagesWithinBudget(archive, options));
1914
+ const hasImages = middle.some(block => block.type === "image");
1915
+ const hasOmittedImages = middle.some(block => block.type === "text");
1838
1916
  if (archive.textHead) {
1839
- const suffix = hasImages
1840
- ? "\n-------------- imaged middle below\n"
1841
- : hasOmittedImages
1842
- ? `\n${omittedFrameNotice(budgeted.omittedFrames, budgeted.omittedBytes)}\n`
1843
- : "";
1917
+ const suffix = hasImages ? "\n-------------- imaged middle below\n" : "";
1844
1918
  blocks.push({ type: "text", text: elideDataUrls(toPlainText(archive.textHead), "archive") + suffix });
1845
- } else if (hasOmittedImages && !hasImages) {
1846
- blocks.push({ type: "text", text: omittedFrameNotice(budgeted.omittedFrames, budgeted.omittedBytes) });
1847
- }
1848
- // Omitted frames are the OLDEST archived images: the byte budget keeps the
1849
- // newest tail frames, so the gap notice precedes the kept images to keep the
1850
- // reconstructed blocks oldest-to-newest.
1851
- if (hasImages && hasOmittedImages) {
1852
- blocks.push({ type: "text", text: omittedFrameNotice(budgeted.omittedFrames, budgeted.omittedBytes) });
1853
1919
  }
1854
- blocks.push(...budgeted.images);
1920
+ blocks.push(...middle);
1855
1921
  if (archive.textTail) {
1856
1922
  const prefix = hasImages
1857
1923
  ? "-------------- imaged middle above\n"