A fresh same-session prior within the TTL is no longer sufficient to price
the text counterfactual warm: deriveBaselineWarmth now also requires the
static-prefix hash (system_sha8) to match. When opencode rotates the system
prompt / tool docs mid-session, the cacheable prefix changes, so a text-only
client would hit a new provider cache key too — pricing it warm against a
cold actual fabricated a huge phantom "loss" (the dashboard's 800%-worse
report). cr>0 still rescues a genuine warm read with no in-memory prior.
Wired through update(), replay(), and aggregateSessions via system_sha8.
Also surface losses honestly in the recent table (Saved/lost: negative
deltas in red instead of hidden as "—") and clarify headings as
billing-equivalent input tokens. Docs updated to match.
Drop the duplicate CHARS_PER_TOKEN=3.7 from export.ts and import REPORT_CHARS_PER_TOKEN from transform.ts so the reporting-estimate constant has one source of truth. Clarify the dashboard baseline-warmth invariant comment. No behavior change.
The savings accounting priced the text baseline as cold whenever pxpipe's own
image cache missed this turn (cr==0), fabricating a 1.25x create the text path
would never have paid and inflating reported savings ~2x. Decouple them: the
text prefix reads warm whenever a fresh same-session prior exists within the
300s TTL (wall-clock), unioned with an observed read (cr>0) for the post-restart
case. Centralised in deriveBaselineWarmth; used by all three call sites
(sessions, dashboard, fragments).
Empirical replay over ~/.pxpipe/events.jsonl (13,402 message rows): honest
headline 504.6M tokens saved vs the old cr-gated 1.05B; 1,409/12,305 fresh-prior
rows (11.4%) read warm despite a busted image cache. Dashboard now narrates the
busted-image case explicitly. Adds baseline/sessions/context-map regression
tests. Measurement only — no change to proxy behavior.
typecheck + 600 tests + build all green.
The magic-word @pxpipe directive fired unreliably and added per-request body
scanning to the hot path, so drop it entirely: removed the directive module +
its test and unwired it from transform.ts / proxy.ts / node.ts / index.ts.
transformRequest now leaves message bodies byte-identical — no per-request
directive parsing.
In its place, `pxpipe export <paths>` renders code/text to dense PNG pages via
the same SDK primitive the proxy uses, so exported pages are byte-identical to
what ships to the model. Width is locked to proxy density (--cols rejected as
an unknown option, guarded by tests) to prevent drift. Added --open to preview
the rendered pages plus a result footer (artifact summary + token-savings line).
Tests: export.test.ts 64 passed (incl. --open parse + default coverage); full
suite 589 passed / 29 files; typecheck clean.
Clears the Node 20 runtime deprecation warning on the runner.
package.json already sets packageManager: pnpm@10.21.0, so action-setup@v6
resolves the pnpm version with no action input - same as the prior bare usage.
Intermittent prefix-cache busts on long sessions (#11): the single cache
breakpoint relocated onto the LAST history image — the still-growing chunk
whose bytes change every window advance — so the slab+history prefix
re-created at 1.25x instead of being read.
- relocateAnchorToHistoryImage pins collapseHistory's carry-over ordinal
(the last byte-stable chunk) instead of the last image.
- demoteProtectedHeadText keeps pxpipe's slab scaffolding (images +
fact-sheet + the [End of rendered context.] boundary) verbatim and only
quarantines the user's stale opening turn, so the relocator can still
find the slab anchor (#14 demotion had clobbered the boundary it keys on).
- cachePrefixDigest joins parts with \x00 so block-boundary differences
can't hash-collide into false prefix-stability.
520/520 green, tsc clean.
Read-only digest of the exact pinned prefix (tools+system+messages through
the imaged history/slab boundary, live tail excluded), computed after final
marker placement and emitted per-turn via tracker. Attributes a prompt-cache
bust as pxpipe-side (sha8 drifts turn-over-turn) vs upstream eviction (sha8
stable while cache_create spikes). Behavior-neutral: byte-identical requests,
518/518 green.
Top-right toggle flips between the warm-light theme and a new dark palette (same flame identity, inverted neutrals). Pure client-side: a <head> script sets the theme before first paint (no flash) from localStorage, defaulting to prefers-color-scheme; no new route or server round-trip. Themed via the existing :root CSS variables plus dark fix-ups for the few hard-coded light spots.
The 64-token budget was filled longest-first, so long low-risk doc-URLs evicted short zero-redundancy tokens (git SHAs, ports, CLI flags, CONST_IDS) — the strings a coding agent must reproduce verbatim. Allocate by priority tier (ids/flags/numbers > paths/versions > URLs, capped); preserve substring-collapse and byte-stable, cache-safe output. Adds an eviction regression test.
writeWebResponse now respects write() backpressure via a drain/close race,
cancels the upstream body reader when the client hangs up, and treats socket
aborts (UND_ERR_SOCKET/ECONNRESET/EPIPE/AbortError, "terminated",
"other side closed") as clean disconnects instead of throwing. Releases the
reader lock and removes the close listener in finally.
Recovers precision-critical strings (paths/ids/versions) from the imaged
slab as cache-stable text so they survive OCR. Byte-identical across turns,
validated on both Claude and gpt-5.5 paths.
Adds the 2026-06-23 entry: pxpipe's right comparison is /compact (itself lossy),
not perfect recall. compact-vs-gist analysis (same job, opposite loss profiles —
pxpipe wins on Fable, peer-on-Opus with abstention, recoverability as the
structural edge); code-grounded notes on the open-source harnesses (codex,
opencode); ties to the agent-memory (reducer/channel-routing) direction. n=1
behavioral parts flagged; eval numbers cross-checked against existing entries.
Docs only — not shipped in the npm package.
Anthropic-path recency anchor for collapsed history (commit 9eb66f5): banner now
warns against reopening low-N turns, plus a bounded ~300-char text pointer to the
most-recent collapsed user turn after the images. Patch — a guardrail fix on the
visible path; cache-safe (pointer sits after the cache_control breakpoint).
Adversarially reviewed: 8 findings, all low/nit — 6 are positive confirmations
(cache-safe, matcher in sync via shared constant, relocation robust to the trailing
block, GPT path unaffected, no off-by-one); 2 benign (tool-result-only user falls
back to an earlier user turn with a correct t-index; mid-word preview truncation).
474 tests pass; tsc + build clean.
GPT autonomous agents (OpenCode/gpt-5.5) no longer lose the live request to
history imaging — the most-recent user turn is pinned as legible text between
before/after history images, with the guard echoing it. Fixes snap/confabulation
/off-task drift; validated on gpt-5.5 (stays on task, no off-task edits).
Patch, not minor: GPT is opt-in/WIP and hidden in the dashboard, and the visible
Anthropic path is byte-identical (pin commit 08a2b0a touched only the GPT path).
Also: clarifying comment on the minCollapseTokens gate (counts imageable work
only; sub-floor histories stay fully text by design — request stays legible).
473 tests pass; tsc + build clean.
Autonomous GPT agents (OpenCode/gpt-5.5) send ONE human request then a long
run of tool turns. The lone request is the OLDEST turn, so history collapse
imaged it first and the model lost it — "I wonder what the user actually asked"
→ off-task drift (observed: agent edited compaction.ts instead of answering a
compare question). The live-request guard's "the request is the trailing user
message" heuristic assumes interactive chat and points at nothing here.
Fix: keep the most-recent user turn OVERALL as legible TEXT, spliced between
before-pin and after-pin history images inside the synthetic user message;
older user turns stay imaged (must not look live — snap-to-first-prompt guard).
The guard now echoes the request verbatim (capped). History is imaged on both
sides of the pin, so compression barely changes.
Cache safety (adversarially reviewed):
- Pin ONLY when the latest user turn is INSIDE the collapse range. If it's in
the kept tail (ordinary interactive turn) it's already native text — pinning
an older in-range turn would migrate the pin across collapse-chunk boundaries
and re-image frozen history. Restricting to the latest turn fixes its position
until the next prompt, so before/after sections stay byte-stable across a run.
- Undersized before-pin remainder merges into the previous before-section rather
than emitting a sub-threshold (net-negative) image.
Both Chat Completions and Responses paths. 473 tests pass; tsc + build clean.
Not yet released — validate on the gpt-5.5 machine first.
b9418bf repriced cr>0-but-no-prior turns warm in dashboard.ts (update + replay)
but left the identical gate in sessions.ts unfixed — so the Sessions panel still
fabricated inflated savings after a restart/eviction or on the first tracked turn
(a cr>0 turn priced cold bills the text counterfactual a 1.25x create on a prefix
we KNOW was cached). Mirror the dashboard fix so "honest cache math" holds on every
panel.
- sessions.ts: warm follows the observed read (cr>0) alone; with no fresh prior,
assume the cacheable prefix was fully reused (prevCacheable = cacheable) rather
than mispricing a known-warm read.
- sessions.test.ts: the "real prefix compression" case had cr=100 on its first
turn (observably warm) yet asserted the cold-priced 22490 fabrication; corrected
to the honest 1790 (warm) + 95 = 1885. Now a regression guard for this fix.
- dashboard.ts: drop the stale "no per-session state" replay comment — replay now
mirrors update()'s per-session warmth.
Found by adversarial review of b9418bf. 462 tests pass; tsc clean.
Collapsed-history images now carry who-said-what AND when, instead of leaving
the model to reconstruct it from a flattened, role-ambiguous blob (which led it
to resurface the opening turn as if it were the live request).
- Turn-index recency anchor: each collapsed turn serializes as <user t="N"> /
<assistant t="N"> with an ABSOLUTE turn index (larger N = more recent), so the
model can tell turn 1 from turn 60. Absolute (never "N ago"/"i of total") so
frozen chunks stay byte-identical and keep hitting the prompt cache. Mirrored on
the GPT path. (history.ts, openai-history.ts, openai.ts)
- colorByRole: role tags are tinted via a parallel "slot string" carried from
serialize time (structure-through), replacing the parse-back that miscolored a
body quoting a literal <tag>. (render.ts, history.ts)
- neutralizeSentinel: a pre-existing ↵ (U+21B5) in content is swapped to ⏎
(U+23CE) in render-prep so reflow packs newlines instead of bailing to a raw,
unpacked render — common when the content is about pxpipe itself. Render-only;
originals preserved. (render.ts, transform.ts, history.ts, openai-history.ts)
- Per-model GPT vision/render profiles, retunable via PXPIPE_GPT_PROFILES,
behavior-identical to the old hardcoded values. (gpt-model-profiles.ts)
Review fixes folded in:
- Banner intro/outro consolidated to a single source of truth; the GPT path now
aliases the Anthropic constant instead of byte-copying it (can't drift).
- slotCopyBody neutralizes literal slot markers to a width-equivalent control
char (U+0003), not a space the minifier would strip and misalign.
459 tests pass; tsc clean.
Collapse old conversation history into rendered PNG sections so the model
reads a compact image instead of re-billed text, while preserving prompt
caching and tool-call behavior. Measures real vs compressed token/cost.
Core:
- GPT history collapse (openai-history.ts): append-only, o200k token-length
sectioning. Sections seal only at a tool-closed boundary (open call-id set
empty), so a function_call and its function_call_output never split across
the collapse cut. Fixes the OpenAI 400 "No tool call found for function
call output with call_id ..." that hit long Responses-API sessions.
- Anthropic cache contract (history.ts): append-only per-chunk rendering;
cache_control markers are preserved/moved, never added; chunk boundaries
align with caller marker seams for byte-stable prefix caching.
- GPT image budget (openai.ts): detail:'original' for gpt-5.x, flagship
vision-multiplier fix, patch cap; schema-strip preserves real descriptions.
- Savings accounting (openai-savings.ts): cached_tokens + vision-token basis.
Model scope (applicability.ts):
- Default imaged scope = claude-fable-5 + gpt-5.6.
- gpt-5.5 and claude-opus-4-8 stay opt-in: same pipeline, but they degrade
reading dense imaged history (gist drift), so silently imaging them by
default is wrong. Promotion is gated on an OCR/recall threshold.
Dashboard: GPT + Anthropic rendering, per-family model toggles, persisted
metrics, thumbnail-expired session UI, reflow/newline handling.
Tests: cache-alignment (GPT + Anthropic), history sectioning + tool-boundary
invariants, savings, dashboard, sessions/restart-restore. 452 passing.
The history image sits after the slab in prefix order, but the single cache
breakpoint was anchored on the slab. So the largest block — the ~141k-token
history image — only cached when the caller's roaming downstream marker happened
to land after it. When it didn't, the whole history image re-created at the 1.25x
cache_create rate every turn instead of amortizing into 0.1x reads. On a warm
session that turned a real compression win into a net loss (the "411% more"
the dashboard correctly surfaced).
Fix is pure relocation: move pxpipe's single breakpoint from the slab onto the
LAST history image (which sits after the slab), so slab + history cache as one
stable prefix — created once, then read. Marker count is unchanged: pxpipe still
never ADDS a marker (Task #21), it only moves the caller's existing one. When no
slab imaged, the history path keeps its own relocation.
- transform.ts: relocateAnchorToHistoryImage on the collapse-success branch
- history.test.ts: two old "history carries no marker" invariants upgraded to
assert the relocation (last history image carries it; exactly one marker total)
replay() rebuilt RecentRow from the JSONL but never set
session_saved_so_far_delta, img_id, or contextHistory — so after a restart
old rows showed "-" in Saved and had no Details link, while live rows did.
Live update() does all three; replay() now matches.
- replay() sets session_saved_so_far_delta (baselineInputEff - actualInputEff,
same cache-weighted math as live) so the Saved column survives a restart.
- replay() rebuilds the Context Map breakdown from persisted fields
(baseline_tokens, bucket_chars, image_count, usage) with a synthetic
nextImageId, and sets img_id/img_ids so the Details link resolves.
- The PNG ring is in-memory and not restorable; restored entries carry
imageIds:[] and a `restored` flag, and the Details panel shows
"thumbnails expired when the proxy restarted" instead of dead <img>s.
- +2 replay tests (restart-restore.test.ts).
376 tests pass; typecheck + build clean.
Claude-Session: https://claude.ai/code/session_01RV8uiMYNhrAdTUzJ9fkpcp
@napi-rs/canvas was moved dependencies -> devDependencies (edge-safe
packaging) but the lockfile still listed it under dependencies, so CI's
`pnpm install --frozen-lockfile` refused the mismatch. Regenerated with
--lockfile-only; no version changes.
Claude-Session: https://claude.ai/code/session_01RV8uiMYNhrAdTUzJ9fkpcp
The session hero computed (1 - sent / raw_count_tokens_baseline) — the same
cache-blind math just removed from the Details panel. It priced the all-text
baseline at full rate, ignoring that most of it would have been cheap
cache-reads, so a warm net-loss session showed a big "fewer tokens" win up top
while Details/Saved showed a loss. Two panels, one session, opposite signs.
- renderSessionSummaryFragment now reads baselineInputWeighted /
actualInputWeighted (the same cache-weighted pair as Details + the Saved
column) instead of rawActualTokens / rawBaselineTokens.
- Headline is input-only; dropped the output-on-both-sides lumping (output is
never "sent" and only dampened the %).
- Honest labels: "effective tokens … after normal cache discounts"; still no $
assumptions (0.1x/1.25x are token multipliers, not $/Mtok).
- types.ts: corrected stale field comments (raw fields are math-drawer only).
- +4 hero tests pinning direction to the weighted pair and that output can't
move the headline %.
374 tests pass; typecheck + build clean.
The Details panel divided the raw count_tokens baseline by sent tokens
(cache-blind), over-claiming "X% smaller" even when the cache-aware row
showed a net loss — the two panels could flatly contradict each other.
- dashboard.ts: feed baselineInputEff/actualInputEff/haveBaseline into
ContextMapData (same weighted pair the recent row uses); push after
the eff-tokens are computed so the breakdown matches.
- fragments.ts: headline divides weighted baseline by weighted actual,
says "bigger" on a net loss, with a clarifying sub-line; no savings
claim when the baseline probe did not resolve.
- +4 tests (context-map.test.ts) pinning headline direction to the row.
370 tests pass; typecheck + build clean.
- Generalize isPxpipeSupportedGptModel to the GPT-5 family (gpt-5, 5.x,
-mini/-nano), default-on; [variant] tags stripped.
- OpenAI render switches to the 768px portrait strip (GPT_STRIP_COLS=152)
so the API's mandatory shortest-side->768 downscale never shrinks our
5px glyphs; downscale-free in both tile and patch regimes.
- Add OpenAI vision-token cost model (resolveVisionCost/openAIVisionTokens)
and rewire the Chat Completions profitability gate to use it instead of
the Anthropic 750px/token model.
- Add transformOpenAIResponses for the /v1/responses (Responses API) shape
used by Codex: instructions + input items + flat tools -> rendered
input_image parts; wire it into the proxy (was passthrough before).
- Tests: +12 (gate family, vision-token math incl. downscale path, chat +
responses transforms). 366 passing; Anthropic path unchanged.
README intentionally not updated (GPT OCR fidelity unverified until tested).
- keepSharp: caller predicate pins exact-match-critical blocks (IDs, hashes,
paths) as text, overriding the chars/token heuristic. Reported via
info.keptSharpBlocks. Never forces imaging; throwing predicate = false.
- emitRecoverable: optional recovery map (block -> original text + provenance)
so a stateful caller can re-inject or re-fetch imaged content. The recover
half of the fidelity contract; documented mitigation for lossy recall.
- edge/Workers-safe: process.env read is typeof-guarded; @napi-rs/canvas moved
to devDependencies (atlas is build-time only, runtime PNG encoder is pure-JS).
- comments: trimmed ~1,640 lines of essay/derivation/changelog narration across
src/ (transform.ts -73%), keeping one-line rationale; deep math points to docs/.
- New exports/types in index.ts + library.ts; +12 tests (keep-sharp, recoverable).
Build clean; 354/354 tests pass.
Tokens are the durable invariant; the dollar figure is a pricing-dependent
consequence that evaporates if Anthropic reprices.
- README: headline + intro now lead with input-token reduction; the
~59-70% end-to-end bill is demoted to a downstream effect 'at current
Fable list prices', with the dollar math pointed to the FAQ
- dashboard: stop claiming Claude's replies are 'never compressed' (false
for any session past keepTail) — scope to latest messages + live output
Captures why imaging conversation history stays cache-safe as a session
grows: the quantized collapseChunk boundary, the one-time cache-create
burn + amortization gate, the protectedPrefix slab anchor, and the
relocate-don't-add cache_control rule. Grounded in src/core/history.ts
and transform.ts; prose mirror of the verified code.
server.close() waits for open connections to drain, so it never finishes while
the dashboard tab polls every 2s or while an SSE response is mid-stream - the
first Ctrl+C printed 'shutting down' but the process never exited.
Drop idle keep-alive sockets immediately, force-close anything still streaming
after a 1.5s grace, and make a second Ctrl+C an instant hard exit.
Full rewrite of src/dashboard/fragments.ts (CSS + render functions + page
shell). Server JSON contracts, htmx endpoints, element IDs, and test-pinned
strings are all preserved.
- Light theme with flame-orange accent (no dark mode)
- One plain-language hero answer on top; all cc=1.25 + cr*0.10 formulas and
counterfactual diagnostics moved behind a 'Show the math' drawer
- Image vs Text Breakdown panel: 'Became an image / Stayed as text' tabs plus
an Image + Source inspector showing the verbatim text behind each page
- Recent table gains a color-coded 'Sent as: image/text' column
- Wide tables (#frag-recent, #frag-stats) and the latest-image inspector
(#frag-latest) scroll inside their card instead of pushing the border out
eval:
- Add eval/lib/cci.py: drop-in interactive Claude session shim (pexpect/PTY)
replacing headless `claude -p`. Drives the real TUI on Max-subscription auth
(no ANTHROPIC_API_KEY), scrapes the reply + /context + /cost, and is
argv/stdin/--output-format json compatible with the old call sites.
- Wire every call site to cci.py: gist-recall (run/run2/run3),
needle-haystack (sweep/crux), gsm8k (bench), swe-bench (run_pilot),
swe-bench-pro (rep_nav/run_pro), and eval/lib/anthropic-client.mjs.
- Isolation per call: --setting-sources project --strict-mcp-config
--dangerously-skip-permissions. Recall evals keep --allowedTools Read;
swe-bench keeps the full agentic toolset. Each trial is a fresh process.
demo:
- Resolve the `opus` shorthand to the full claude-opus-4-8[1m] model in
cost-ab and effective-context a.sh/b.sh. setup.sh intentionally keeps the
base id: the proxy scope gate strips [1m] from the request before matching,
so [1m]-on-launch + base-in-scope is the correct pairing.
docs:
- FINDINGS.md: drop the 2026-06-16 Opus 4.8 re-test sections.