Commit Graph

309 Commits

Author SHA1 Message Date
Danil Silantyev ee94cb323b test(docs): recursive link + heading-anchor integrity check
Broaden the guard from README + top-level docs/*.md inline links to the full
tracked doc set and more link forms:

- scan README.md, CHANGELOG.md, FINDINGS.md, docs/**/*.md, and eval/**/README.md
  (eval run logs are skipped — only their READMEs are prose)
- parse inline links, image links, reference-style definitions, and simple HTML
  href=/src=
- validate the target file exists AND, when a link carries a #fragment into a
  markdown file, that a matching GitHub-style heading anchor exists

Verified: passes on the current tree and fails on an injected broken link or
broken #anchor. (A README public-API-example typecheck lands on the integration
branch, where the README import fix is present.)
2026-07-04 22:57:55 -04:00
Danil Silantyev ad6c918e06 test(docs): guard README/docs relative links against rot
A static audit can only catch broken doc links once; this keeps them honest on
every run. A dependency-light vitest test (pure fs + regex, no new deps, already
picked up by `pnpm test` and CI) walks README.md and docs/*.md, extracts each
relative link/image target (skipping http/mailto/data/# and stripping
fragments/queries), and asserts the target exists.

Verified: passes on the current tree (all 7 relative links resolve, including
docs/LEGIBILITY-AUDIT-2026-07-01.md) and fails with a clear per-link message
when a dead link is introduced.
2026-07-04 22:57:55 -04:00
Danil Silantyev 6c14465ca4 test(openai): prove Responses outgoingTextChars excludes image base64
Strengthen the >0 assertion into a structural one: with a rendered image, a
pointer, an input_text prompt, and a rewritten tool in the outgoing body, assert
outgoingTextChars (a) counts the instructions pointer + input_text parts + tool
name/description/parameters (>= their summed length, within the '\n\n' part
separators), and (b) stays well below the input_image base64 length — i.e. the
base64 is excluded from the regression denominator, as intended.
2026-07-04 20:15:11 -04:00
Danil Silantyev 4c4b77c17b fix(openai): record outgoingTextChars for Responses transforms
transformOpenAIChatCompletions set info.outgoingTextChars = countOutgoingTextChars(req)
before returning; transformOpenAIResponses set info.compressed = true and returned
without it. That field is the regression denominator (tokens ≈ α·outgoingTextChars +
β·imagePixels), so compressed Responses rows silently carried no denominator.

- add countResponsesOutgoingTextChars(): instructions + message-item text
  (string or input_text parts, via responsesContentText so input_image base64 is
  excluded) + flat tool name/description/parameters
- set it on the Responses success path, mirroring the Chat path
- test asserts outgoingTextChars > 0 for a compressed Responses request
2026-07-04 20:15:11 -04:00
Danil Silantyev 0e4a61695f fix(applicability): match all proxy Anthropic message routes
shouldTransformAnthropicMessages() gated the path with
!path.endsWith('/v1/messages'), but createProxy() routes three exact paths:
/v1/messages, /anthropic/v1/messages, and /anthropic/messages. So an SDK/host
using the public helper to pre-gate got unsupported_path on /anthropic/messages
— a request the proxy actually transforms — while endsWith would also have
wrongly accepted an unrelated /foo/v1/messages.

- lift isAnthropicMessagesPath() into applicability.ts (proxy.ts already imports
  from it; no cycle) as the single source of truth
- proxy.ts imports it instead of its own copy
- shouldTransformAnthropicMessages() uses it, so helper and router agree
- public-api tests cover /anthropic/v1/messages and /anthropic/messages
  (eligible) plus /v1/messages/count_tokens (still unsupported_path)
2026-07-04 19:25:51 -04:00
Danil Silantyev aff960f025 chore(tsconfig): drop stale dashboard build comment and vestigial exclude
The exclude comment described src/dashboard as "the Svelte browser bundle —
compiled separately by scripts/build-dashboard-ui.mjs ... covered by
tsconfig.dashboard.json for editor + CI type-checking." None of that is true:

- src/dashboard/ is server-rendered HTML (htmx + Alpine, both inlined by
  scripts/vendor-ui.mjs) — not Svelte, and there is no esbuild-svelte step
- scripts/build-dashboard-ui.mjs and tsconfig.dashboard.json do not exist
- CI (.github/workflows/ci.yml) runs only the root typecheck/test/build

The exclude itself was a no-op: src/dashboard.ts (which IS in the program)
imports ./dashboard/fragments.js and ./dashboard/types.js, so tsc already
type-checks and emits dist/dashboard/* transitively. Verified: removing the
exclude leaves typecheck green and dist/dashboard/* emits identically with or
without it. Dropped both the false comment and the dead exclude entry.
2026-07-04 19:13:57 -04:00
Danil Silantyev 6ee31d6847 fix(openai): image array-form Responses system/developer content
The Responses API allows a message item's content to be a string OR an array of
input_text parts. transformOpenAIResponses only read the string form
(`typeof content === 'string' ? content : ''`), so a developer/system item sent
as [{ type: 'input_text', text: ... }] was neither imaged nor stubbed — its
verbose text rode uncompressed as native input, and the pointer replacement
(also string-only) left it in place.

- collect static context via the existing responsesContentText() helper, which
  reads both shapes
- pointer replacement now handles arrays too, preserving the array shape
  ([{ type: 'input_text', text: RESPONSES_POINTER }]) so a parts-form request is
  not reshaped into a string
- test: a developer item with array content is imaged (staticChars counts it)
  and its outgoing content is a single pointer part, no original text
2026-07-04 19:09:42 -04:00
Danil Silantyev 6a5656f20b test(export): end-to-end --git untracked filtering in a real git repo
The original bug lived in collectSource's --git untracked branch, not in the
readExportTextFile helper the unit tests cover. Add an E2E test that runs the
actual CLI (tsx src/node.ts export --git --include '*.ts') against a throwaway
git repo with a tracked baseline plus untracked keep.ts, a 5000-char skip.md, an
oversized huge.ts, and a binary bin.ts. Asserts oversized + binary untracked
files are skipped (with warnings) and that only keep.ts's content reaches the
source (sourceChars < 500 — skip.md would have added thousands).
2026-07-04 19:06:35 -04:00
Danil Silantyev 27e60d0c17 fix(export): apply include/exclude and size limit to --git untracked files
In --git mode, untracked files (git ls-files --others) were read straight to
memory with only a binary check — unlike directory and single-file modes, which
also apply --include/--exclude globs and the 1 MiB MAX_FILE_BYTES cap. So
`pxpipe export --git --include '*.ts'` still slurped every untracked file
regardless of the filter, and an untracked multi-MB file was read whole (a
resource-safety hazard).

- extract the shared gate into src/export-collect.ts: readExportTextFile()
  applies include/exclude → size → binary, in that order, and is import-safe
  (src/core/export.ts is contractually fs-free, so it can't live there)
- route all three collection paths (walkDir, single-file target, --git
  untracked) through it; untracked skips now warn like explicit targets do
- tests/export-collect.test.ts covers include, exclude, oversized, the size
  boundary, binary, and inaccessible

Verified end-to-end: `export --git --include '*.ts'` in a temp repo excludes an
untracked .md, skips an oversized .ts (with a warning), and keeps the .ts.
2026-07-04 19:06:35 -04:00
Danil Silantyev 69c9f2e83c fix(cli): print the real package version for --version
printVersion() logged `process.env.npm_package_version ?? '0.2.0'`. npm only
populates npm_package_version inside its own run-script env, so an end user
running `npx pxpipe-proxy --version` or a globally-installed `pxpipe` got the
stale hardcoded fallback (0.2.0) instead of the real version — the code comment
even claimed an esbuild.define that was never wired up.

- scripts/build.mjs reads package.json and inlines the version via esbuild
  `define: { __PXPIPE_VERSION__ }`, then smoke-checks the built binary
  (`node dist/node.js --version` must equal package.json) and fails the build
  on mismatch, so a broken injection can never ship.
- src/node.ts reads the injected constant behind a `typeof` guard (safe under
  tsx, where it is undefined), falling back to npm_package_version then
  'unknown' — never a stale release number.

Verified: node dist/node.js --version now prints 0.8.0.
2026-07-04 18:50:48 -04:00
Danil Silantyev 3f61348a19 docs(readme): fix Library use example — package name and renderTextToImages
The 'Library use' snippet imported `renderTextToPngs` from "pxpipe", but the
published package is "pxpipe-proxy" and the root barrel (src/core/index.ts)
exports `renderTextToImages`, not `renderTextToPngs`. Both the import
specifier and the function name were unrunnable as written.

- import from "pxpipe-proxy" (matches package.json "name")
- use renderTextToImages, which returns { pages, droppedChars, pixels }
- show pages[i].png (Uint8Array) instead of the stale RenderedImage[] comment
2026-07-04 18:47:16 -04:00
teamchong 7dd54d395d release: v0.8.0 v0.8.0 2026-07-03 22:47:19 -04:00
teamchong 0c514f3220 fix(worker): require PXPIPE_WORKER_SECRET when an API-key override is set
If ANTHROPIC_API_KEY / OPENAI_API_KEY is configured on a deployed
Worker, the URL becomes an open key-spender: anyone who discovers it
gets requests signed with the deployment's key. Now:

- keys set + no PXPIPE_WORKER_SECRET -> 503, refuses to proxy at all
- keys set + secret -> callers must send x-pxpipe-secret (compared via
  SHA-256 digests to avoid a prefix-timing signal); 401 otherwise
- no keys set -> unchanged (callers bring their own credentials)

The secret is stripped before forwarding upstream.
2026-07-03 22:30:50 -04:00
teamchong 72e3894530 fix(dashboard): drop access-control-allow-origin:* from JSON endpoints
The dashboard HTML is served from the same origin as the JSON API, so
same-origin fetches never consult CORS. The wildcard header only served
cross-origin readers - i.e. any webpage open in the operator's browser
could read captured context out of 127.0.0.1. Browser default (opaque
cross-origin responses) is exactly what we want here.
2026-07-03 22:30:50 -04:00
teamchong aa39fcea31 docs(demo): pin demo URLs to 127.0.0.1 to match the IPv4 loopback bind 2026-07-03 21:52:29 -04:00
Sutha Kamal 75263d64c3 fix(node): bind dashboard to loopback by default (HOST to opt into all-interfaces)
The Node proxy called server.listen(port) with no host, so it bound all
interfaces (0.0.0.0/::) while the startup log printed http://127.0.0.1.
The dashboard is unauthenticated and serves captured request context
(/api/image-source returns the source text rendered into images) plus a
compression kill switch, with access-control-allow-origin: *. On a shared
network that context was readable by anyone who could reach the port.

Bind to 127.0.0.1 by default; add a HOST env to explicitly opt into
all-interfaces exposure (container/host-access use cases), with a startup
warning when bound off-loopback. IPv6 handled: ::1/localhost count as
loopback (no warning) and the startup URL is bracket-formatted. The one
README client example that used localhost is pinned to 127.0.0.1 to match
the IPv4 loopback default on IPv6-first hosts.

Verified: default refuses off-host connections; HOST=0.0.0.0 restores the
previous behavior + warns; HOST=::1 binds IPv6 loopback with a valid
bracketed URL and no warning. typecheck + build pass.
2026-07-03 21:46:55 -04:00
teamchong ea7d526cad docs(readme): cut to 217 lines — dedupe caveats, move benchmark prose to eval/ links
Launch feedback: too long. Structural trims only; FAQ answers, demo
captions, and all measured numbers kept verbatim.
2026-07-03 19:30:33 -04:00
teamchong 3294c0d15c fix(history): stop teaching the model to skip Reads
- staleFreshnessHints() rewrites the upstream "(file state is current in
  your context - no need to Read it back)" hint into a stale-state warning
  at history-serialization time, covering both collapsed same-session
  turns and slabs inherited by continuation sessions
- Edit/Write/NotebookEdit stubs gain a live-text same-session Read
  precondition (READ_FIRST_TOOLS), riding un-imaged in the tools array
- telemetry: churning_static_tags canary - FNV-1a fingerprint per
  session/tag flags slab tags whose content churns and busts the image
  cache (TAG_OBSERVATIONS_MAX-capped)
- 5 regression tests in tests/history.test.ts; baseline to beat: 97
  read-gate errors across 18 sessions
2026-07-03 19:00:39 -04:00
Steven 9fa16eef54 Fix image link for pxpipe vs plain Claude demo
Updated image link for Opus 4.8 demo in README.
2026-07-03 11:35:26 -04:00
Steven 79c5e56862 Revise demo details for Fable 5 and Opus 4.8
Updated demo sections for Fable 5 and Opus 4.8, including new comparisons and performance metrics.
2026-07-03 11:25:23 -04:00
teamchong 2070c9697e release: v0.7.2 v0.7.2 2026-07-03 09:02:00 -04:00
teamchong c21520bd14 fix(history): carry opening-turn task text verbatim past demotion (#7)
Opening-turn task prompts were reduced to a 300-char compactPreview
tombstone by demoteProtectedHeadText, losing questions and trailing
output-format instructions that no later turn restated. The tombstone
keeps its cheap cache-stable preview, but latestCollapsedUserPointer
now scans INCLUDING the protected head: when the opening turn is the
only typed user text in the collapsed range (single-task sessions),
the pointer carries it verbatim via verbatimTaskText — up to 4000
chars, head 2600 / tail 1400 elision beyond that.
2026-07-03 08:23:41 -04:00
teamchong 0dce007d4c release: v0.7.1 v0.7.1 2026-07-03 01:50:27 -04:00
teamchong de0483cf51 fix(transform): wrap relocated env text in <system-reminder> for correct attribution 2026-07-03 01:50:08 -04:00
teamchong bb7aaa9fec release: v0.7.0 v0.7.0 2026-07-03 00:40:59 -04:00
teamchong ed8be0bfea docs: changelog for 0.7.0 2026-07-03 00:40:59 -04:00
teamchong b7de47eb3f docs: add Fable 5 A/B demo (same answers, ~9x cheaper) + attempt log
- README: Fable clip as headline demo (Drive link + committed thumb),
  verified numbers from the recording: plain $42.21 / 96% context vs
  pxpipe $4.51; honest caveat on single-reply format compliance
- ATTEMPTS.md: attempt 1 (invalid run, model-switch pitfall) and
  attempt 2 (legibility PASS on fable, format miss documented)
- a.sh/b.sh: prompt fix — numbering starts at filler-000
2026-07-03 00:37:06 -04:00
teamchong ece33030e0 Add headless bench: multi-turn claude -p driver + events.jsonl scorer
bench/run.sh: builds, boots an isolated proxy (auto free port, own-log
readiness check so a stale listener can't absorb the run), seeds a git
workspace from demo/cost-ab/template, churns volatile env state between
turns, then scores the run's events.jsonl.

bench/score.mjs: per-session + overall cost_units (1.25x create / 0.1x
read), prefix flips, cold restarts, flip waste, stop_reason histogram,
safety flags; writes score.json; two args = A/B diff.
2026-07-02 23:27:34 -04:00
teamchong 06a8b70c01 fix(transform): relocate volatile env text behind ALL cache breakpoints
Volatile env/context text (git status, cwd, date) rode in req.system —
BEFORE the slab anchor in Anthropic's prefix order (tools → system →
messages) — so any git-state change cold-restarted the entire anchored
prefix. Telemetry attribution (events.jsonl 2026-06-26..07-02): 48.8%
of cold-create waste, ~2.6k tokens/session vs ~200 saved by imaging it.

Now appended as a trailing text block on the LAST user message — the
per-turn live tail that re-caches incrementally anyway — placed AFTER
history collapse (never baked into frozen chunks) and after tool_result
compression. Session-stable billingLine/sysRemainder stay in system;
fallback keeps env in system when no user message exists to carry it.
New telemetry field: envRelocatedChars.
2026-07-02 23:17:13 -04:00
teamchong e49bb11878 feat(telemetry): log stop_reason + safety-flag per request 2026-07-02 23:07:37 -04:00
teamchong 42a3b363d5 fix(demo): add --no-chrome to both cost-ab arms
The Chrome extension attaches over its own channel (--strict-mcp-config
does not cover it) and injects tools mid-session, mutating the tool
array and busting the prompt-cache prefix from that point on. This
landed asymmetrically between arms and inflated b.sh's cache-write
cost. With --no-chrome the tool surface is frozen from request 1 in
both arms; a fresh A/B pair confirmed: $1.26 control vs $0.78 pxpipe,
cache writes 46.8k vs 22.5k, no write fail, no safeguard trip.
2026-07-02 22:24:13 -04:00
teamchong beacd67b4a fix(transform): freeze imaged slab at first render — keep volatile content (skill listings, cwd caches) out of the imaged prefix so turn-2 system sha matches turn-1 2026-07-02 21:11:18 -04:00
teamchong 0ad3c68664 fix(transform): cross-session slab stability + drop compressSchemas knob
Fix #1 (cross-session cache bust): strip the volatile "# Environment"
markdown section (working dir, git status, platform, model ID) out of
the system text BEFORE the static/dynamic split. It carries no XML
wrapper so splitStaticDynamic couldn't catch it, and its git-status
lines change across sessions - baking them into the slab PNG busted
the cross-session cache (system_sha8 717f1fce -> 5efaa4bb for a
one-file edit). Parallel to stripBillingLine: the section re-enters
the system tail as plain text.

Schema handling simplified, knob removed: the Anthropic text-reference
path now renders prose-only tool docs (schemas stay untouched in
tools[] - duplicating them in the reference was pure bloat), while the
GPT imaged path always carries the compact schema (there it IS the
compression). Deletes the {type:'object'} stub + SCHEMA_STRUCTURAL_KEYS
machinery, stripSchemaDescriptions import, and the COMPRESS_SCHEMAS env.

Tests: e2e regressions for anchor relocation onto byte-frozen carry-over
history images and first-collapse anchor staying on the slab; history
relocation fixture grown past the collapseChunk=50 snap (56 messages ->
cutoff 50, frozen windows 11/21/31/41, collapsedTurns 49 - the old
35-message fixture floored to one window and never froze a chunk).

tsc clean; 615/615 tests pass.
2026-07-02 17:36:18 -04:00
teamchong baa1e1ee01 fix(transform): reword tool-docs stub/header to avoid refusal classifier
The text-based Tool Reference triggered Anthropic's model-cloning
safeguard (stop_reason: refusal -> silent Opus fallback) because the
per-tool stub said docs lived in "the system prompt" — the exact
phrasing the 169521c retrip identified as the trigger for the imaged
slab, now reintroduced in text form.

- Stub now reads: 'ⓘ Full docs: see "## Tool: <name>" in the Tool
  Reference section.' — no "system prompt"/"authoritative" framing.
- Tool Reference header is provenance-framed as first-party ("pxpipe
  (this user's local proxy) moved the full tool documentation...").
- tracker.ts now maps tool_docs_chars into events.jsonl so refusals
  are attributable to the text-reference path.
- Regression test: stub + reference header must never match
  /system prompt|authoritative/i and must carry provenance framing
  (scoped to the header; quoted third-party tool docs below it may
  legitimately contain the phrase).

Verified: 614/614 tests, tsc --noEmit clean, and a live cold-start
through the rebuilt proxy on claude-fable-5 with the text reference
active — no refusal event, no fallback.
2026-07-02 13:17:39 -04:00
teamchong 169521c9f8 fix(transform): defuse reasoning_extraction refusal — provenance-framed slab banner
The in-image banner announced 'SYSTEM PROMPT + TOOL DOCS ... treat as
authoritative system instructions' inside a user turn. Anthropic's
model-cloning classifier read that as a replayed/extracted prompt and
refused (apiRefusalCategory: reasoning_extraction), forcing Claude Code
to fall back to claude-opus-4-8 — outside compress scope, so the whole
session ran passthrough with zero savings.

Reworded to first-party provenance framing (pxpipe rendered this
session's own configuration; no 'system prompt'/'authoritative').
Two headless probes through :47824 now serve on claude-fable-5[1m]
with the imaged slab (img:4, ~94k chars) and zero refusal events;
identical shape was refused pre-fix (req_011CccRnutLzRJNVZ7bRKG8Q).
2026-07-01 22:25:08 -04:00
teamchong 7f2e40bad6 Revert "feat(transform): PXPIPE_TEXT_SLAB=1 keeps the system slab as text (classifier-safe)"
This reverts commit c04e8f80e8.
2026-07-01 21:50:39 -04:00
teamchong c04e8f80e8 feat(transform): PXPIPE_TEXT_SLAB=1 keeps the system slab as text (classifier-safe)
Anthropic's safety classifier OCRs rendered images. A *legible* image of the
system prompt + tool docs (bash / permissions / credential / file-write
vocabulary) trips its prompt-injection heuristic and forces a model downgrade
to Opus, which costs far more than imaging the (already prompt-cached) slab
ever saves. Empirically the caption wording is NOT the trigger — the OCR'd
content is — so no caption rewrite fixes it; the slab must stay text.

Gate at the slab profitability check with PXPIPE_TEXT_SLAB=1. Default is
unchanged (legacy imaged slab) so the cache-align / cache-stability suites
stay valid. Both demos now launch the pxpipe proxy with the toggle set.

Verified: identical ~300KB-slab request images 8 PNGs by default, 0 with the
toggle (reason=slab_text). Full suite: 614 passed.
2026-07-01 19:46:29 -04:00
teamchong ed968a7495 Reapply "fix(render): fit Anthropic page to 1568×728 (~1.15 MP) for WYSIWYG glyphs"
This reverts commit 6eaa8bfcd0.
2026-07-01 19:18:20 -04:00
teamchong 6eaa8bfcd0 Revert "fix(render): fit Anthropic page to 1568×728 (~1.15 MP) for WYSIWYG glyphs"
This reverts commit 838db8778e.
2026-07-01 18:50:25 -04:00
teamchong 9cade86868 debug(node): add PXPIPE_DUMP_DIR to persist rendered PNGs for demo inspection 2026-07-01 18:50:25 -04:00
teamchong ad947335e5 demo: run fable arm on claude-fable-5[1m] (1M ctx), matching opus/sonnet
Both demos' a.sh/b.sh now request claude-fable-5[1m] for the default `fable`
arm, consistent with the opus/sonnet arms which already use [1m].

setup.sh PXPIPE_MODELS stays the BASE claude-fable-5 on purpose: the proxy
strips [variant] tags from the incoming model before matching against the
allow-list verbatim (src/core/applicability.ts), so base claude-fable-5
already covers claude-fable-5[1m]. Writing [1m] into the allow-list would
make the stripped incoming base no longer match -> pxpipe silently stops
compressing. Added a comment in each setup.sh pinning that.
2026-07-01 18:06:53 -04:00
teamchong 838db8778e fix(render): fit Anthropic page to 1568×728 (~1.15 MP) for WYSIWYG glyphs
Measured 2026-07-01 (count_tokens sweep, claude-sonnet-4-5; see
docs/LEGIBILITY-AUDIT-2026-07-01.md): the API downscales every image to fit
BOTH long-edge ≤1568 AND ~1.15 MP (≈1,143,750 px), then bills ≈px/750
(~1525 tok/img cap). The old 1932×1932 page hit the cap but was resampled
0.555× before the vision encoder, so 5×8 glyphs arrived at ~2.8×4.4 px — the
root cause of the legibility misses this session.

New page shape 1568×728 = 1,141,504 px fits both bounds → glyphs reach the
encoder WYSIWYG (and still ≤2000 px/side for >20-image requests).

render.ts geometry:
  MAX_HEIGHT_PX          1932  -> 728
  MAX_WIDTH_PX           1932  -> 1568
  DEFAULT_COLS            313  -> 312  (1568 px exactly; 313=1573 would 0.997× blur)
  DENSE_CONTENT_COLS     384  -> 312
  READABLE/DENSE chars  50000/92160 -> 28080  (312×90, tracks real pagination)

Decouple the GPT path: OpenAI resizes differently (2048 bbox, 768 short side),
so a 768-wide strip up to 2048 tall survives un-resampled. Add
GPT_MAX_HEIGHT_PX = 1932 (gpt-model-profiles.ts) where the built-in cost
numbers were calibrated; openai-history.ts now uses it instead of the
now-Anthropic-specific MAX_HEIGHT_PX. Doc-only touch-ups in library.ts/export.ts.

tests/render + tests/paging updated to the new geometry (171 pass).
2026-07-01 17:54:51 -04:00
teamchong d7999279a5 eval: add constant-cost render-style A/B harness (paused task #7 arms)
Holds the glyph cell at prod 5x8 and varies ONLY render style
(prod/onebit/color/grid/cgrid) at identical pixel dims => identical
image-token cost. Answers what the size sweep could not: at fixed cost,
does any style beat the ~10% exact-read baseline? Same content, grader,
and claude-CLI read method as eval/glyph-matrix/sweep.
2026-07-01 16:41:24 -04:00
teamchong b453adf0c1 docs(node): pin why the transform hook is only a kill switch
The function-form transform intentionally returns {} on the active
path; the gate runs on static DEFAULTS (charsPerToken=4, priorWarm*=0)
and there is no live-alpha feedback loop from the dashboard. Record the
telemetry that justified this (2026-06: 897 sessions / 21,347 measured
rows, 5 mode flips ever, losses 0.8% of wins, all cache-create
amortization) so nobody "fixes" it without re-running that
reconciliation.
2026-07-01 16:41:12 -04:00
teamchong edf7a05f85 feat(dashboard): tag one-time cache-create losses in recent table
Negative Saved/lost cells get a small "create" pill (with explanatory
tooltip) when repricing the newly written prefix at the read rate turns
the loss positive: saved + cache_create x (1.25 - 0.1) > 0. Those rows
are the one-time cache-write premium being amortized, not gate
failures. Rates/token counts interpolate from CACHE_CREATE_RATE /
CACHE_READ_RATE so the copy cannot drift from the math.

Verified against ~/.pxpipe/events.jsonl replay: 3/3 negative cells in
the live 50-row window marked; warm-turn losses (cc=0) correctly bare.
2026-07-01 16:41:12 -04:00
teamchong a346ec7ac5 feat(factsheet): carry occurrence counts, match ticket-style codes
- extractFactSheetEntries() returns {token, count}; the kept-token set
  and order are byte-identical to the old extractFactSheetTokens(),
  which now delegates. Counts render as a ×N suffix, so tally questions
  over imaged content are answerable from the text sheet instead of by
  counting 5x8 px glyph rows.
- Same-token spans matched by overlapping patterns dedupe by offset
  (token + match index) so nothing double-counts.
- New tier-0 shape: uppercase hyphenated codes with a digit
  (PROJ-1482, CVE-2024-30078); digit lookahead is bounded.
- export.ts consumes entries across all pages; 14 new/extended tests.
2026-07-01 16:41:00 -04:00
teamchong d1704241d6 demo: add claude-sonnet-5 support alongside opus
a.sh/b.sh  -> claude-sonnet-5[1m] (was the stale claude-sonnet-4-6),
matching the opus alias's [1m] pattern. setup.sh  -> claude-sonnet-5
(bare id, feeds PXPIPE_MODELS). claude-sonnet-5 was already in the dashboard
model catalog (fragments.ts); old 4.6 still reachable via the verbatim-id
fallback. No src/ changes.
2026-06-30 15:22:20 -04:00
teamchong f472566cc1 release: v0.6.10
Claude-Session: https://claude.ai/code/session_014gJXMzmULNmtuNhbhSdjVo
v0.6.10
2026-06-30 12:45:08 -04:00
Steven Chong bf49065dce fix(savings): use observed cache state for text baseline 2026-06-30 12:24:09 -04:00
Steven Chong ff8a05e30c fix(savings): gate text-counterfactual warmth on static-prefix hash
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.
2026-06-29 15:59:10 -04:00