mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
feat(transform): cache_prefix_sha8 prefix-cache bust attribution probe (#11)
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.
This commit is contained in:
@@ -76,6 +76,12 @@ export interface TrackEvent {
|
||||
/** sha8 of the collapsed history image. Unchanged across turns proves the prompt cache is hitting (cache_read).
|
||||
* A drifting hash means the collapse boundary is unstable. Absent on no-collapse turns. */
|
||||
history_image_sha8?: string;
|
||||
/** sha8 of the exact cacheable prefix sent (tools+system+imaged prefix, live
|
||||
* tail excluded). Changes turn-over-turn within a session ⇒ pxpipe-side cache
|
||||
* bust; stable while cache_create spikes ⇒ upstream eviction. See #11. */
|
||||
cache_prefix_sha8?: string;
|
||||
/** Approx chars in that pinned prefix (growth vs pure-invalidation split). */
|
||||
cache_prefix_bytes?: number;
|
||||
|
||||
// From TransformInfo.env:
|
||||
cwd?: string;
|
||||
@@ -235,6 +241,8 @@ export function toTrackEvent(ev: ProxyEvent): TrackEvent {
|
||||
if (info.historyImageSha) {
|
||||
out.history_image_sha8 = info.historyImageSha;
|
||||
}
|
||||
if (info.cachePrefixSha8) out.cache_prefix_sha8 = info.cachePrefixSha8;
|
||||
if (info.cachePrefixBytes !== undefined) out.cache_prefix_bytes = info.cachePrefixBytes;
|
||||
if (info.unknownStaticTags && info.unknownStaticTags.length > 0)
|
||||
out.unknown_static_tags = info.unknownStaticTags;
|
||||
if (info.systemSha8) out.system_sha8 = info.systemSha8;
|
||||
|
||||
@@ -563,6 +563,16 @@ export interface TransformInfo {
|
||||
* proves Anthropic's prompt cache can `cache_read` (0.1×) instead of `cache_create`.
|
||||
* A changing hash means cache-key drift is back. Only set when collapse produced images. */
|
||||
historyImageSha?: string;
|
||||
/** sha8 of the ACTUAL cacheable prefix sent this turn (tools + system +
|
||||
* message blocks through the imaged history/slab boundary; the live tail is
|
||||
* excluded). Read-only measurement. A change turn-over-turn within a session
|
||||
* ⇒ pxpipe serialized different prefix bytes (we busted our own cache,
|
||||
* pxpipe-side); STABLE while cache_create spikes / cache_read collapses ⇒ the
|
||||
* prefix was evicted upstream. Decisive attribution signal (see #11). */
|
||||
cachePrefixSha8?: string;
|
||||
/** Approx size (chars) of that cached prefix — pairs with cachePrefixSha8 so a
|
||||
* bust reads as growth (size up) vs pure invalidation (size unchanged). */
|
||||
cachePrefixBytes?: number;
|
||||
/** Why the history collapse didn't run (or did). Diagnostic only. */
|
||||
historyReason?:
|
||||
| 'no_history'
|
||||
@@ -779,6 +789,52 @@ function relocateAnchorToHistoryImage(messages: Message[] | undefined): void {
|
||||
delete slabAnchor.cache_control;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only digest of the cacheable prefix pxpipe actually sends: tools +
|
||||
* system + message blocks up to and including the imaged history image (or, on
|
||||
* no-collapse turns, the slab boundary). The naturally-growing live tail is
|
||||
* excluded, so the digest only moves when something *inside the pinned prefix*
|
||||
* moves. Pairs with per-turn cache_read/cache_create to attribute a prompt-cache
|
||||
* bust: a digest that CHANGES between consecutive turns of one session means we
|
||||
* serialized different prefix bytes (pxpipe-side — a per-turn block crossing the
|
||||
* breakpoint, or marker drift); a STABLE digest on a turn that still re-created
|
||||
* the prefix points upstream (eviction). Never mutates the request, so it cannot
|
||||
* perturb the cache behavior it measures.
|
||||
*/
|
||||
async function cachePrefixDigest(
|
||||
req: { tools?: unknown; system?: unknown; messages?: unknown },
|
||||
): Promise<{ sha8: string; bytes: number } | undefined> {
|
||||
const msgs = Array.isArray(req.messages) ? (req.messages as Message[]) : [];
|
||||
// Boundary = latest message carrying pxpipe's imaged prefix: the history image
|
||||
// (banner) when collapse ran, else the slab message ('[End of rendered
|
||||
// context.]'). Identified exactly as relocateAnchorToHistoryImage does.
|
||||
let boundary = -1;
|
||||
for (let i = 0; i < msgs.length; i++) {
|
||||
const content = msgs[i]?.content;
|
||||
if (!Array.isArray(content)) continue;
|
||||
const first = content[0] as TextBlock | undefined;
|
||||
const isHistory = first?.type === 'text' && first.text === HISTORY_SYNTHETIC_INTRO;
|
||||
const hasSlab = content.some(
|
||||
(b) => b && (b as TextBlock).type === 'text' && (b as TextBlock).text === '[End of rendered context.]',
|
||||
);
|
||||
if (isHistory || hasSlab) boundary = i;
|
||||
}
|
||||
if (boundary < 0) return undefined; // not an imaged-prefix shape — nothing pinned
|
||||
const parts: string[] = [];
|
||||
if (Array.isArray(req.tools)) for (const t of req.tools) parts.push(JSON.stringify(t));
|
||||
const sys = req.system;
|
||||
if (typeof sys === 'string') parts.push(sys);
|
||||
else if (Array.isArray(sys)) for (const b of sys) parts.push(JSON.stringify(b));
|
||||
for (let i = 0; i <= boundary; i++) {
|
||||
const content = msgs[i]?.content;
|
||||
if (typeof content === 'string') parts.push(content);
|
||||
else if (Array.isArray(content))
|
||||
for (const b of content) parts.push(typeof b === 'string' ? b : JSON.stringify(b));
|
||||
}
|
||||
const prefix = parts.join(' | ||||