@oh-my-pi/snapcompact 16.4.6 → 16.5.0

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
+ ## [16.5.0] - 2026-07-13
6
+
7
+ ### Changed
8
+
9
+ - Updated archived transcript rendering to use a more compact format with `¶user:`, `¶think:`, `¶ai:`, and `¶call:` scopes, omitting repeated adjacent scope headers and appending tool-call intents as comments.
10
+
5
11
  ## [16.3.7] - 2026-07-05
6
12
 
7
13
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/snapcompact",
4
- "version": "16.4.6",
4
+ "version": "16.5.0",
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.4.6",
35
- "@oh-my-pi/pi-natives": "16.4.6",
36
- "@oh-my-pi/pi-utils": "16.4.6",
37
- "@oh-my-pi/pi-wire": "16.4.6"
34
+ "@oh-my-pi/pi-ai": "16.5.0",
35
+ "@oh-my-pi/pi-natives": "16.5.0",
36
+ "@oh-my-pi/pi-utils": "16.5.0",
37
+ "@oh-my-pi/pi-wire": "16.5.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/bun": "^1.3.14"
@@ -1,6 +1,9 @@
1
1
  You are resuming a prior conversation. Its earlier turns were archived to reclaim context and are reproduced under HISTORY below, oldest to newest. Read HISTORY in full, then continue from the live conversation that follows it.
2
2
 
3
- The archived transcript is compact: each turn opens with a heading — `# User ¶`, `# Assistant ¶`, or `# Tool call ¶` — assistant reasoning is wrapped in _italics_, and tool output sits inside `<out>…</out>`.
3
+ The archived transcript uses compact scopes:
4
+ - `¶user:`, `¶think:`, `¶ai:`, and `¶call:` open user, assistant reasoning, assistant reply, and tool-call scopes.
5
+ - Following lines without a `¶…:` prefix remain in the current scope. Consecutive blocks of the same kind omit the repeated prefix.
6
+ - A tool call reads `¶call:name(args)//intent`; the trailing `//intent` is optional. Its `<out>…</out>` block is tool output.
4
7
 
5
8
  Reading HISTORY:
6
9
  - Plain-text sections are the verbatim transcript — rely on them exactly.
@@ -9,9 +12,7 @@ Reading HISTORY:
9
12
  {{else}} - A frame is one grid {{cols}} characters wide and up to {{rows}} rows tall: read left to right, top to bottom — there is no word wrap, so a word may break across rows.
10
13
  {{/if}}{{#if sentenceInk}} - Ink cycles through six colors, one per sentence.
11
14
  {{/if}}{{#if stopwordDimmed}} - Function words are dim gray; content words keep full ink.
12
- {{/if}}{{#if dimmedToolResults}} - Text inside `<out>` is dim gray — that gray is archived tool output, not conversation.
13
15
  {{/if}}{{#if lineRepeated}} - Each line is printed twice (white, then a pale-yellow band); the two copies are identical.
14
- {{/if}}{{#if mixedShapes}} - The compressed middle frames use a smaller, denser font than the edge frames; the reading order is unchanged.
15
16
  {{/if}}{{/if}}{{#if includedPreviousSummary}}- HISTORY opens with a condensed digest of still-older context that predates the archived turns.
16
17
  {{/if}}{{#if truncatedChars}}- About {{truncatedChars}} characters of older middle history were dropped to fit the archive budget.
17
18
  {{/if}}- When an exact earlier detail matters and a section reads unclearly, re-derive it from the workspace (re-read files, re-run commands) rather than guessing.
@@ -638,8 +638,6 @@ export function createFileOps(): FileOperations {
638
638
  }
639
639
  const URL_SCHEME_RE = /[a-z][a-z0-9+.-]*:\/\//i;
640
640
 
641
- const HEADING_MARKER = " ¶";
642
-
643
641
  export function isUrlSchemePath(path: string): boolean {
644
642
  return URL_SCHEME_RE.test(path);
645
643
  }
@@ -776,10 +774,22 @@ export function serializeConversation(messages: Message[], options?: SerializeOp
776
774
  const headRatio = options?.truncateHeadRatio ?? TRUNCATE_HEAD_RATIO;
777
775
  const dimToolResults = options?.dimToolResults !== false;
778
776
  const parts: string[] = [];
777
+ let lastPrefix: string | null = null;
778
+
779
+ const pushPart = (prefix: string, content: string) => {
780
+ const lastIndex = parts.length - 1;
781
+ if (lastIndex >= 0 && lastPrefix === prefix) {
782
+ const sep = parts[lastIndex].endsWith("\n") || content.startsWith("\n") ? "" : "\n";
783
+ parts[lastIndex] += sep + content;
784
+ } else {
785
+ parts.push(prefix + content);
786
+ lastPrefix = prefix;
787
+ }
788
+ };
779
789
 
780
790
  // Tool results flagged contextually useless (and their paired calls) carry no
781
791
  // information worth archiving — skip the whole pair. Surviving results are
782
- // indexed by tool-call id so each merges into its originating `# Tool call`.
792
+ // indexed by tool-call id so each merges into its originating `¶call:` scope.
783
793
  const uselessCallIds = new Set<string>();
784
794
  const resultTextByCallId = new Map<string, string>();
785
795
  for (const msg of messages) {
@@ -796,7 +806,7 @@ export function serializeConversation(messages: Message[], options?: SerializeOp
796
806
  }
797
807
 
798
808
  // Wrap a raw tool-result body in an `<out>` block, dimming only the body so
799
- // the frame coloring keeps structure (headings, calls) loud.
809
+ // the frame coloring keeps scope markers and calls loud.
800
810
  const renderResultBlock = (rawText: string): string => {
801
811
  const body = truncateForSummary(stripDimMarkers(rawText), toolResultMaxChars, headRatio);
802
812
  return `<out>\n${dimToolResults ? `${DIM_ON}${body}${DIM_OFF}` : body}\n</out>`;
@@ -813,18 +823,19 @@ export function serializeConversation(messages: Message[], options?: SerializeOp
813
823
  .filter((content): content is { type: "text"; text: string } => content.type === "text")
814
824
  .map(content => content.text)
815
825
  .join("");
816
- if (content) parts.push(`# User${HEADING_MARKER}\n${stripDimMarkers(content)}`);
826
+ if (content) pushPart("¶user:", stripDimMarkers(content));
817
827
  } else if (msg.role === "assistant") {
818
828
  // Stream blocks in content order: buffer thinking/text, then flush a
819
- // `# Assistant` block (thinking as italics above the text) right before
820
- // each tool call, so text or thinking after a call stays after it.
829
+ // separate section for each block type right before each tool call.
821
830
  let pendingThinking: string[] = [];
822
831
  let pendingText: string[] = [];
823
832
  const flushAssistant = () => {
824
- const sections: string[] = [];
825
- if (pendingThinking.length > 0) sections.push(`_${pendingThinking.join("\n")}_`);
826
- if (pendingText.length > 0) sections.push(pendingText.join("\n"));
827
- if (sections.length > 0) parts.push(`# Assistant${HEADING_MARKER}\n${sections.join("\n\n")}`);
833
+ if (pendingThinking.length > 0) {
834
+ pushPart("¶think:", pendingThinking.join("\n"));
835
+ }
836
+ if (pendingText.length > 0) {
837
+ pushPart("¶ai:", pendingText.join("\n"));
838
+ }
828
839
  pendingThinking = [];
829
840
  pendingText = [];
830
841
  };
@@ -860,24 +871,27 @@ export function serializeConversation(messages: Message[], options?: SerializeOp
860
871
  toolCallMaxChars,
861
872
  headRatio,
862
873
  );
863
- const lines = [`# Tool call${HEADING_MARKER}`];
864
- if (intent) lines.push(`//${intent}`);
865
- lines.push(`${block.name}(${argsStr})`);
874
+ const lines: string[] = [];
875
+ let firstLine = `${block.name}(${argsStr})`;
876
+ if (intent) {
877
+ firstLine += `//${intent}`;
878
+ }
879
+ lines.push(firstLine);
866
880
  const resultText = resultTextByCallId.get(block.id);
867
881
  if (resultText !== undefined) {
868
882
  mergedCallIds.add(block.id);
869
883
  lines.push(renderResultBlock(resultText));
870
884
  }
871
- parts.push(lines.join("\n"));
885
+ pushPart("¶call:", lines.join("\n"));
872
886
  }
873
887
  }
874
888
  flushAssistant();
875
889
  } else if (msg.role === "toolResult") {
876
- // Paired results already merged into their `# Tool call` block above;
890
+ // Paired results already merged into their tool call block above;
877
891
  // only orphans (call archived outside this window) render standalone.
878
892
  if (uselessCallIds.has(msg.toolCallId) || mergedCallIds.has(msg.toolCallId)) continue;
879
893
  const resultText = resultTextByCallId.get(msg.toolCallId);
880
- if (resultText !== undefined) parts.push(`# Tool call${HEADING_MARKER}\n${renderResultBlock(resultText)}`);
894
+ if (resultText !== undefined) pushPart("¶call:", `\n${renderResultBlock(resultText)}`);
881
895
  }
882
896
  }
883
897
 
@@ -1911,15 +1925,6 @@ export async function compact<TMessage = Message>(
1911
1925
 
1912
1926
  const frames = await Promise.all(newFrames);
1913
1927
  const totalChars = frames.reduce((sum, frame) => sum + frame.chars, 0) + textChars;
1914
- const mixedShapes = frames.some(
1915
- frame =>
1916
- frame.cols !== geo.cols ||
1917
- frame.rows !== geo.rows ||
1918
- (frame.variant ?? "sent") !== high.variant ||
1919
- (frame.lineRepeat ?? 1) !== high.lineRepeat ||
1920
- (frame.columns ?? 1) !== (high.columns ?? 1) ||
1921
- (frame.stopwordDim ?? false) !== (high.stopwordDim ?? false),
1922
- );
1923
1928
 
1924
1929
  const { readFiles, modifiedFiles } = computeFileLists(fileOps);
1925
1930
  const files = formatFileList(readFiles, modifiedFiles, fileOps.read);
@@ -1936,9 +1941,7 @@ export async function compact<TMessage = Message>(
1936
1941
  rows: geo.rows,
1937
1942
  sentenceInk: high.variant === "sent",
1938
1943
  stopwordDimmed: high.stopwordDim === true,
1939
- dimmedToolResults: options?.dimToolResults !== false,
1940
1944
  lineRepeated: high.lineRepeat > 1,
1941
- mixedShapes,
1942
1945
  truncatedChars,
1943
1946
  includedPreviousSummary,
1944
1947
  files: files.length > 0 ? files : undefined,