@animalabs/membrane 0.5.78 → 0.5.80

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.
Files changed (58) hide show
  1. package/dist/cache-keepalive.d.ts +115 -0
  2. package/dist/cache-keepalive.d.ts.map +1 -0
  3. package/dist/cache-keepalive.js +0 -0
  4. package/dist/cache-keepalive.js.map +1 -0
  5. package/dist/cache-keepalive.test.d.ts +2 -0
  6. package/dist/cache-keepalive.test.d.ts.map +1 -0
  7. package/dist/cache-keepalive.test.js +206 -0
  8. package/dist/cache-keepalive.test.js.map +1 -0
  9. package/dist/floating-cache-marker.test.d.ts +2 -0
  10. package/dist/floating-cache-marker.test.d.ts.map +1 -0
  11. package/dist/floating-cache-marker.test.js +242 -0
  12. package/dist/floating-cache-marker.test.js.map +1 -0
  13. package/dist/formatters/anthropic-xml.d.ts.map +1 -1
  14. package/dist/formatters/anthropic-xml.js +30 -2
  15. package/dist/formatters/anthropic-xml.js.map +1 -1
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/membrane.d.ts +7 -1
  21. package/dist/membrane.d.ts.map +1 -1
  22. package/dist/membrane.js +148 -5
  23. package/dist/membrane.js.map +1 -1
  24. package/dist/providers/anthropic.d.ts +20 -0
  25. package/dist/providers/anthropic.d.ts.map +1 -1
  26. package/dist/providers/anthropic.js +23 -3
  27. package/dist/providers/anthropic.js.map +1 -1
  28. package/dist/providers/bedrock.d.ts.map +1 -1
  29. package/dist/providers/bedrock.js +31 -2
  30. package/dist/providers/bedrock.js.map +1 -1
  31. package/dist/types/config.d.ts +5 -0
  32. package/dist/types/config.d.ts.map +1 -1
  33. package/dist/types/config.js.map +1 -1
  34. package/dist/types/content.d.ts +5 -0
  35. package/dist/types/content.d.ts.map +1 -1
  36. package/dist/types/content.js.map +1 -1
  37. package/dist/types/request.d.ts +13 -0
  38. package/dist/types/request.d.ts.map +1 -1
  39. package/dist/types/tools.d.ts +19 -0
  40. package/dist/types/tools.d.ts.map +1 -1
  41. package/dist/utils/tool-parser.d.ts +16 -2
  42. package/dist/utils/tool-parser.d.ts.map +1 -1
  43. package/dist/utils/tool-parser.js +143 -35
  44. package/dist/utils/tool-parser.js.map +1 -1
  45. package/package.json +3 -2
  46. package/src/cache-keepalive.test.ts +244 -0
  47. package/src/cache-keepalive.ts +385 -0
  48. package/src/floating-cache-marker.test.ts +261 -0
  49. package/src/formatters/anthropic-xml.ts +30 -2
  50. package/src/index.ts +13 -0
  51. package/src/membrane.ts +143 -5
  52. package/src/providers/anthropic.ts +46 -3
  53. package/src/providers/bedrock.ts +32 -1
  54. package/src/types/config.ts +6 -0
  55. package/src/types/content.ts +5 -0
  56. package/src/types/request.ts +14 -0
  57. package/src/types/tools.ts +20 -0
  58. package/src/utils/tool-parser.ts +147 -36
@@ -0,0 +1,115 @@
1
+ /** Minimal shape we need back from a keepalive send. */
2
+ export interface KeepaliveUsage {
3
+ cache_read_input_tokens?: number | null;
4
+ cache_creation_input_tokens?: number | null;
5
+ }
6
+ export type KeepaliveSend = (wire: Record<string, unknown>, headers: Record<string, string> | undefined) => Promise<{
7
+ usage?: KeepaliveUsage;
8
+ }>;
9
+ export type KeepaliveLane = 'stream' | 'complete';
10
+ export type KeepaliveEvent = {
11
+ type: 'refreshed';
12
+ key: string;
13
+ lane: KeepaliveLane;
14
+ readTokens: number;
15
+ idleMs: number;
16
+ } | {
17
+ type: 'ineffective';
18
+ key: string;
19
+ reason: string;
20
+ readTokens: number;
21
+ writeTokens: number;
22
+ } | {
23
+ type: 'skipped';
24
+ key: string;
25
+ reason: string;
26
+ } | {
27
+ type: 'error';
28
+ key: string;
29
+ error: string;
30
+ consecutive: number;
31
+ } | {
32
+ type: 'disabled';
33
+ reason: string;
34
+ } | {
35
+ type: 'expired';
36
+ key: string;
37
+ idleMs: number;
38
+ };
39
+ export interface CacheKeepaliveConfig {
40
+ /** Master switch. Default true. */
41
+ enabled?: boolean;
42
+ /**
43
+ * Stop refreshing once the last REAL request is this old. Keepalive pokes do
44
+ * not extend this — otherwise an agent that never speaks again would be kept
45
+ * warm forever. Default 24h.
46
+ */
47
+ maxIdleMs?: number;
48
+ /**
49
+ * Refresh once the entry hasn't been touched for this long. Must be < the
50
+ * cache TTL, with margin: the TTL clock starts at the *start* of the request,
51
+ * and a long streaming turn can itself eat minutes. Default 45m against a 1h
52
+ * TTL leaves 15m of headroom.
53
+ */
54
+ refreshAfterMs?: number;
55
+ /** Timer cadence. Default 5m. */
56
+ checkIntervalMs?: number;
57
+ /**
58
+ * Which lanes to keep warm. Default ['stream'] — the primary/voice lane.
59
+ * The aux ('complete') lane is measured to do no prompt caching at all today
60
+ * (fable-cm: 382 aux calls, every one create=0/read=0), so warming it would
61
+ * poke a cache entry that does not exist.
62
+ */
63
+ lanes?: KeepaliveLane[];
64
+ /** LRU cap on tracked lineages, to bound memory. Each holds a full wire
65
+ * request (~1.5MB for a 500k-token resident). Default 4. */
66
+ maxLineages?: number;
67
+ /** Consecutive send failures before the whole keepalive disables itself. */
68
+ maxConsecutiveErrors?: number;
69
+ /**
70
+ * How many times a lineage may come back as a WRITE instead of a read before
71
+ * we stop poking it. A lineage whose prefix churns every turn cannot be kept
72
+ * warm, and paying 2x to discover that repeatedly is the worst outcome.
73
+ */
74
+ maxIneffective?: number;
75
+ onEvent?: (event: KeepaliveEvent) => void;
76
+ }
77
+ /**
78
+ * Reasons a request shape cannot be safely replayed as `max_tokens: 0`.
79
+ *
80
+ * Each of these is either rejected outright by the API, or — worse — would
81
+ * require editing the request in a way that moves the cache-invalidation
82
+ * boundary. Skipping is always cheaper than guessing.
83
+ */
84
+ export declare function ineligibleReason(wire: Record<string, unknown>): string | null;
85
+ /**
86
+ * Identity of a cache lineage: model + system + tools. This is exactly the root
87
+ * of the cached prefix, so two agents in one process, or an agent's primary vs
88
+ * aux lane, land in different buckets automatically.
89
+ */
90
+ export declare function lineageKey(wire: Record<string, unknown>): string;
91
+ export declare class CacheKeepalive {
92
+ private readonly send;
93
+ private lineages;
94
+ private timer;
95
+ private ticking;
96
+ private consecutiveErrors;
97
+ private stopped;
98
+ private readonly cfg;
99
+ constructor(send: KeepaliveSend, config?: CacheKeepaliveConfig);
100
+ /** Record a real outbound request. Cheap; called on every LLM call. */
101
+ record(wire: Record<string, unknown>, headers: Record<string, string> | undefined, lane: KeepaliveLane): void;
102
+ private ensureTimer;
103
+ private tick;
104
+ private refresh;
105
+ private emit;
106
+ /** Snapshot for operators / tests. */
107
+ getStatus(): Array<{
108
+ key: string;
109
+ lane: KeepaliveLane;
110
+ idleMs: number;
111
+ realIdleMs: number;
112
+ }>;
113
+ stop(): void;
114
+ }
115
+ //# sourceMappingURL=cache-keepalive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-keepalive.d.ts","sourceRoot":"","sources":["../src/cache-keepalive.ts"],"names":[],"mappings":"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"}
Binary file
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-keepalive.js","sourceRoot":"","sources":["../src/cache-keepalive.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cache-keepalive.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-keepalive.test.d.ts","sourceRoot":"","sources":["../src/cache-keepalive.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,206 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { CacheKeepalive, ineligibleReason, lineageKey, } from './cache-keepalive.js';
3
+ /** A wire request shaped like what buildRequest() produces for a real turn. */
4
+ function wire(overrides = {}) {
5
+ return {
6
+ model: 'claude-fable-5',
7
+ max_tokens: 32000,
8
+ stream: true,
9
+ thinking: { type: 'adaptive' },
10
+ system: [
11
+ { type: 'text', text: 'You are a resident.', cache_control: { type: 'ephemeral', ttl: '1h' } },
12
+ ],
13
+ messages: [
14
+ {
15
+ role: 'user',
16
+ content: [
17
+ { type: 'text', text: 'hello', cache_control: { type: 'ephemeral', ttl: '1h' } },
18
+ ],
19
+ },
20
+ ],
21
+ ...overrides,
22
+ };
23
+ }
24
+ const hit = { usage: { cache_read_input_tokens: 494_000, cache_creation_input_tokens: 0 } };
25
+ describe('ineligibleReason', () => {
26
+ it('accepts a normal 1h-cached adaptive-thinking request', () => {
27
+ expect(ineligibleReason(wire())).toBeNull();
28
+ });
29
+ it('rejects legacy budget_tokens thinking (max_tokens:0 is refused, and we must not disable thinking to fix it)', () => {
30
+ expect(ineligibleReason(wire({ thinking: { type: 'enabled', budget_tokens: 8000 } })))
31
+ .toBe('legacy-thinking-budget');
32
+ });
33
+ it('rejects forced tool_choice, which cannot be substituted without invalidating the messages cache', () => {
34
+ expect(ineligibleReason(wire({ tool_choice: { type: 'any' } }))).toBe('forced-tool-choice');
35
+ expect(ineligibleReason(wire({ tool_choice: { type: 'tool', name: 'x' } }))).toBe('forced-tool-choice');
36
+ expect(ineligibleReason(wire({ tool_choice: { type: 'auto' } }))).toBeNull();
37
+ });
38
+ it('rejects structured output, which the API refuses with max_tokens:0', () => {
39
+ expect(ineligibleReason(wire({ output_config: { format: { type: 'json_schema' } } })))
40
+ .toBe('structured-output');
41
+ });
42
+ it('rejects a request with no cache_control at all (the compression lane today)', () => {
43
+ const aux = wire({ system: undefined, messages: [{ role: 'user', content: 'compress this' }] });
44
+ expect(ineligibleReason(aux)).toBe('no-cache-breakpoint');
45
+ });
46
+ it('rejects a 5m-only request: poking a 5m entry every few minutes costs more than it saves', () => {
47
+ const short = wire({
48
+ system: [{ type: 'text', text: 's', cache_control: { type: 'ephemeral' } }],
49
+ messages: [{ role: 'user', content: [{ type: 'text', text: 'h', cache_control: { type: 'ephemeral', ttl: '5m' } }] }],
50
+ });
51
+ expect(ineligibleReason(short)).toBe('no-1h-breakpoint');
52
+ });
53
+ });
54
+ describe('lineageKey', () => {
55
+ it('is stable across turns that only append messages', () => {
56
+ const a = wire();
57
+ const b = wire({ messages: [...wire().messages, { role: 'assistant', content: 'hi' }] });
58
+ expect(lineageKey(a)).toBe(lineageKey(b));
59
+ });
60
+ it('separates different agents, models, and tool sets', () => {
61
+ expect(lineageKey(wire())).not.toBe(lineageKey(wire({ model: 'claude-opus-5' })));
62
+ expect(lineageKey(wire())).not.toBe(lineageKey(wire({ system: [{ type: 'text', text: 'different agent' }] })));
63
+ expect(lineageKey(wire())).not.toBe(lineageKey(wire({ tools: [{ name: 't' }] })));
64
+ });
65
+ });
66
+ describe('CacheKeepalive', () => {
67
+ beforeEach(() => vi.useFakeTimers());
68
+ afterEach(() => vi.useRealTimers());
69
+ const setup = (send, cfg = {}) => {
70
+ const events = [];
71
+ const ka = new CacheKeepalive(send, {
72
+ refreshAfterMs: 45 * 60_000,
73
+ checkIntervalMs: 5 * 60_000,
74
+ maxIdleMs: 24 * 60 * 60_000,
75
+ onEvent: (e) => events.push(e),
76
+ ...cfg,
77
+ });
78
+ return { ka, events };
79
+ };
80
+ it('does nothing while real traffic keeps the entry warm (the busy-agent case)', async () => {
81
+ const send = vi.fn().mockResolvedValue(hit);
82
+ const { ka } = setup(send);
83
+ // A turn every 10 minutes for 2 hours: real requests refresh the TTL for
84
+ // free, so the keepalive must never fire.
85
+ for (let i = 0; i < 12; i++) {
86
+ ka.record(wire(), undefined, 'stream');
87
+ await vi.advanceTimersByTimeAsync(10 * 60_000);
88
+ }
89
+ expect(send).not.toHaveBeenCalled();
90
+ ka.stop();
91
+ });
92
+ it('refreshes once the entry goes untouched past refreshAfterMs', async () => {
93
+ const send = vi.fn().mockResolvedValue(hit);
94
+ const { ka, events } = setup(send);
95
+ ka.record(wire(), undefined, 'stream');
96
+ await vi.advanceTimersByTimeAsync(40 * 60_000);
97
+ expect(send).not.toHaveBeenCalled();
98
+ await vi.advanceTimersByTimeAsync(10 * 60_000);
99
+ expect(send).toHaveBeenCalledTimes(1);
100
+ expect(events.some((e) => e.type === 'refreshed')).toBe(true);
101
+ ka.stop();
102
+ });
103
+ it('replays max_tokens:0 without stream, and changes nothing else', async () => {
104
+ const send = vi.fn().mockResolvedValue(hit);
105
+ const { ka } = setup(send);
106
+ const original = wire();
107
+ ka.record(original, { 'anthropic-beta': 'x' }, 'stream');
108
+ await vi.advanceTimersByTimeAsync(50 * 60_000);
109
+ const call = send.mock.calls[0];
110
+ const payload = call[0];
111
+ const headers = call[1];
112
+ expect(payload.max_tokens).toBe(0);
113
+ expect('stream' in payload).toBe(false);
114
+ expect(headers).toEqual({ 'anthropic-beta': 'x' });
115
+ // The cache-key-bearing fields must be untouched — this is the whole point.
116
+ expect(payload.thinking).toEqual({ type: 'adaptive' });
117
+ expect(payload.system).toBe(original.system);
118
+ expect(payload.messages).toBe(original.messages);
119
+ expect(payload.model).toBe('claude-fable-5');
120
+ ka.stop();
121
+ });
122
+ it('stops refreshing after maxIdleMs measured from the last REAL request', async () => {
123
+ const send = vi.fn().mockResolvedValue(hit);
124
+ const { ka, events } = setup(send, { maxIdleMs: 3 * 60 * 60_000 });
125
+ ka.record(wire(), undefined, 'stream');
126
+ await vi.advanceTimersByTimeAsync(3 * 60 * 60_000 + 60_000);
127
+ // Pokes must not extend their own mandate: ~3 refreshes, then expiry.
128
+ expect(events.some((e) => e.type === 'expired')).toBe(true);
129
+ const before = send.mock.calls.length;
130
+ await vi.advanceTimersByTimeAsync(6 * 60 * 60_000);
131
+ expect(send.mock.calls.length).toBe(before);
132
+ ka.stop();
133
+ });
134
+ it('detects a poke that WROTE instead of read, and gives up on that lineage', async () => {
135
+ // The silent-20x failure: a successful call that created a fresh 2x entry.
136
+ const send = vi.fn().mockResolvedValue({
137
+ usage: { cache_read_input_tokens: 0, cache_creation_input_tokens: 494_000 },
138
+ });
139
+ const { ka, events } = setup(send, { maxIneffective: 2 });
140
+ ka.record(wire(), undefined, 'stream');
141
+ await vi.advanceTimersByTimeAsync(50 * 60_000);
142
+ await vi.advanceTimersByTimeAsync(50 * 60_000);
143
+ const bad = events.filter((e) => e.type === 'ineffective');
144
+ expect(bad.length).toBe(2);
145
+ expect(bad[0].reason).toBe('wrote-instead-of-read');
146
+ const after = send.mock.calls.length;
147
+ await vi.advanceTimersByTimeAsync(4 * 60 * 60_000);
148
+ expect(send.mock.calls.length).toBe(after);
149
+ ka.stop();
150
+ });
151
+ it('treats an absent usage field as "not a read" rather than assuming success', async () => {
152
+ const send = vi.fn().mockResolvedValue({});
153
+ const { ka, events } = setup(send);
154
+ ka.record(wire(), undefined, 'stream');
155
+ await vi.advanceTimersByTimeAsync(50 * 60_000);
156
+ expect(events.some((e) => e.type === 'ineffective')).toBe(true);
157
+ ka.stop();
158
+ });
159
+ it('hard-disables after consecutive errors instead of becoming a retry storm', async () => {
160
+ // fable-cm, 2026-08-21: 1033 `400 invalid_request_error` in 3h from a
161
+ // background lane. A keepalive must never be able to do that.
162
+ const send = vi.fn().mockRejectedValue(new Error('400 invalid_request_error'));
163
+ const { ka, events } = setup(send, { maxConsecutiveErrors: 3 });
164
+ ka.record(wire(), undefined, 'stream');
165
+ await vi.advanceTimersByTimeAsync(24 * 60 * 60_000);
166
+ expect(send.mock.calls.length).toBe(3);
167
+ expect(events.some((e) => e.type === 'disabled')).toBe(true);
168
+ ka.stop();
169
+ });
170
+ it('ignores the aux lane by default', async () => {
171
+ const send = vi.fn().mockResolvedValue(hit);
172
+ const { ka } = setup(send);
173
+ ka.record(wire(), undefined, 'complete');
174
+ await vi.advanceTimersByTimeAsync(2 * 60 * 60_000);
175
+ expect(send).not.toHaveBeenCalled();
176
+ ka.stop();
177
+ });
178
+ it('evicts the oldest lineage past maxLineages to bound memory', () => {
179
+ const send = vi.fn().mockResolvedValue(hit);
180
+ const { ka } = setup(send, { maxLineages: 2 });
181
+ ka.record(wire({ model: 'a' }), undefined, 'stream');
182
+ ka.record(wire({ model: 'b' }), undefined, 'stream');
183
+ ka.record(wire({ model: 'c' }), undefined, 'stream');
184
+ expect(ka.getStatus().length).toBe(2);
185
+ ka.stop();
186
+ });
187
+ it('drops a snapshot when the request shape stops being warmable', () => {
188
+ const send = vi.fn().mockResolvedValue(hit);
189
+ const { ka, events } = setup(send);
190
+ ka.record(wire(), undefined, 'stream');
191
+ expect(ka.getStatus().length).toBe(1);
192
+ // Same lineage (model/system/tools identical), now with forced tool choice.
193
+ ka.record(wire({ tool_choice: { type: 'any' } }), undefined, 'stream');
194
+ expect(ka.getStatus().length).toBe(0);
195
+ expect(events.some((e) => e.type === 'skipped')).toBe(true);
196
+ ka.stop();
197
+ });
198
+ it('is inert when disabled', async () => {
199
+ const send = vi.fn().mockResolvedValue(hit);
200
+ const ka = new CacheKeepalive(send, { enabled: false });
201
+ ka.record(wire(), undefined, 'stream');
202
+ await vi.advanceTimersByTimeAsync(6 * 60 * 60_000);
203
+ expect(send).not.toHaveBeenCalled();
204
+ });
205
+ });
206
+ //# sourceMappingURL=cache-keepalive.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-keepalive.test.js","sourceRoot":"","sources":["../src/cache-keepalive.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=floating-cache-marker.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floating-cache-marker.test.d.ts","sourceRoot":"","sources":["../src/floating-cache-marker.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Floating cache marker: incremental prompt caching inside the native
3
+ * tool loop (see the doctrine block in buildNativeToolRequest).
4
+ *
5
+ * Context strategies place breakpoints once per turn at compile time; the
6
+ * tool loop rebuilds the request every round with an append-only suffix
7
+ * the strategy never saw. The float rides the newest message using only
8
+ * the RESIDUAL breakpoint budget — upstream markers are never displaced.
9
+ * Motivating incident: qa-ops 2026-08-20, ~5.3M uncached tokens in 18min
10
+ * from two subagents whose single marker sat at message 2 of 61.
11
+ */
12
+ import { describe, it, expect, vi, afterEach } from 'vitest';
13
+ import { Membrane } from './membrane.js';
14
+ // ---------------------------------------------------------------------------
15
+ // Fixtures
16
+ // ---------------------------------------------------------------------------
17
+ const text = (t) => ({ type: 'text', text: t });
18
+ const user = (t, bp = false) => ({
19
+ participant: 'User',
20
+ content: [text(t)],
21
+ ...(bp ? { cacheBreakpoint: true } : {}),
22
+ });
23
+ const assistantToolCall = (id) => ({
24
+ participant: 'Claude',
25
+ content: [
26
+ text('running a tool'),
27
+ { type: 'tool_use', id, name: 'shell', input: { cmd: 'ls' } },
28
+ ],
29
+ });
30
+ const toolResults = (id, bp = false) => ({
31
+ participant: 'User',
32
+ content: [{ type: 'tool_result', toolUseId: id, content: 'ok' }],
33
+ ...(bp ? { cacheBreakpoint: true } : {}),
34
+ });
35
+ /** A turn: marked kickoff + `rounds` completed tool rounds. */
36
+ function turn(rounds, kickoffMarked = true) {
37
+ const messages = [user('do the thing', kickoffMarked)];
38
+ for (let i = 1; i <= rounds; i++) {
39
+ messages.push(assistantToolCall(`t${i}`), toolResults(`t${i}`));
40
+ }
41
+ return messages;
42
+ }
43
+ function makeRequest(overrides = {}) {
44
+ return {
45
+ messages: [],
46
+ system: 'You are a test agent.',
47
+ config: { model: 'claude-sonnet-5', maxTokens: 128 },
48
+ tools: [{ name: 'shell', description: 'run a command', inputSchema: { type: 'object' } }],
49
+ promptCaching: true,
50
+ cacheTtl: '1h',
51
+ assistantParticipant: 'Claude',
52
+ ...overrides,
53
+ };
54
+ }
55
+ function build(request, messages, rebuild, membrane) {
56
+ const m = membrane ?? new Membrane({ name: 'anthropic' });
57
+ return m.buildNativeToolRequest(request, messages, rebuild);
58
+ }
59
+ /** Total cache_control instances across the whole wire request. */
60
+ function totalMarkers(pr) {
61
+ let n = 0;
62
+ pr.messages.forEach((m) => (Array.isArray(m.content) ? m.content : []).forEach((b) => { if (b.cache_control)
63
+ n++; }));
64
+ if (Array.isArray(pr.tools))
65
+ pr.tools.forEach((t) => { if (t.cache_control)
66
+ n++; });
67
+ if (Array.isArray(pr.system))
68
+ pr.system.forEach((b) => { if (b.cache_control)
69
+ n++; });
70
+ return n;
71
+ }
72
+ /** [messageIndex, blockIndex] of every message-level cache_control, plus tools/system markers. */
73
+ function markers(pr) {
74
+ const msgs = [];
75
+ pr.messages.forEach((m, mi) => {
76
+ (Array.isArray(m.content) ? m.content : []).forEach((b, bi) => {
77
+ if (b.cache_control)
78
+ msgs.push([mi, bi]);
79
+ });
80
+ });
81
+ const onTools = Array.isArray(pr.tools) && pr.tools.some((t) => t.cache_control);
82
+ const onSystem = Array.isArray(pr.system) && pr.system.some((b) => b.cache_control);
83
+ return { messages: msgs, onTools, onSystem };
84
+ }
85
+ const last = (pr) => pr.messages.length - 1;
86
+ afterEach(() => {
87
+ vi.restoreAllMocks();
88
+ });
89
+ // ---------------------------------------------------------------------------
90
+ // Tests
91
+ // ---------------------------------------------------------------------------
92
+ describe('floating cache marker', () => {
93
+ it('does not float on the turn\'s first build', () => {
94
+ const pr = build(makeRequest(), turn(1), false);
95
+ // Only the strategy's kickoff marker; the round's suffix is unmarked.
96
+ expect(markers(pr).messages).toEqual([[0, 0]]);
97
+ });
98
+ it('floats onto the newest message on a tool-loop rebuild', () => {
99
+ const pr = build(makeRequest(), turn(1), true);
100
+ const m = markers(pr);
101
+ // Kickoff marker intact + float on the final tool_result envelope.
102
+ expect(m.messages).toContainEqual([0, 0]);
103
+ expect(m.messages).toContainEqual([last(pr), 0]);
104
+ // Fallback stays suppressed: message markers exist.
105
+ expect(m.onTools).toBe(false);
106
+ expect(m.onSystem).toBe(false);
107
+ });
108
+ it('keeps the previous round\'s endpoint marked when budget allows', () => {
109
+ const pr = build(makeRequest(), turn(2), true);
110
+ const m = markers(pr);
111
+ // kickoff + previous round's results envelope + newest envelope.
112
+ expect(m.messages).toEqual([[0, 0], [last(pr) - 2, 0], [last(pr), 0]]);
113
+ });
114
+ it('never stacks a second marker on an already-marked final message', () => {
115
+ const messages = turn(2);
116
+ messages[messages.length - 1].cacheBreakpoint = true;
117
+ const pr = build(makeRequest(), messages, true);
118
+ const m = markers(pr);
119
+ const onFinal = m.messages.filter(([mi]) => mi === last(pr));
120
+ expect(onFinal).toHaveLength(1);
121
+ // Budget not consumed by the dedupe: previous endpoint still floated.
122
+ expect(m.messages).toContainEqual([last(pr) - 2, 0]);
123
+ });
124
+ it('withholds the float (with one warning) when upstream markers fill all 4 slots', () => {
125
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
126
+ const messages = turn(3);
127
+ // Mark every results envelope: 1 kickoff + 3 results = 4 upstream markers.
128
+ for (const msg of messages) {
129
+ if (msg.content[0].type === 'tool_result')
130
+ msg.cacheBreakpoint = true;
131
+ }
132
+ const membrane = new Membrane({ name: 'anthropic' });
133
+ const pr = build(makeRequest(), messages, true, membrane);
134
+ expect(markers(pr).messages).toHaveLength(4);
135
+ expect(warn).toHaveBeenCalledTimes(1);
136
+ // Warn-once: a second rebuild does not warn again.
137
+ build(makeRequest(), messages, true, membrane);
138
+ expect(warn).toHaveBeenCalledTimes(1);
139
+ });
140
+ it('floats from the residuum left after the tools/system fallback on markerless requests', () => {
141
+ const pr = build(makeRequest(), turn(2, false), true);
142
+ const m = markers(pr);
143
+ // Fallback spent 2 (tools + system) → residuum 2 → newest + previous endpoint.
144
+ expect(m.onTools).toBe(true);
145
+ expect(m.onSystem).toBe(true);
146
+ expect(m.messages).toEqual([[last(pr) - 2, 0], [last(pr), 0]]);
147
+ });
148
+ it('steps back off a trailing thinking block', () => {
149
+ const messages = turn(1);
150
+ messages.push({
151
+ participant: 'Claude',
152
+ content: [text('partial'), { type: 'thinking', thinking: 'hmm', signature: 'sig' }],
153
+ });
154
+ const pr = build(makeRequest(), messages, true);
155
+ const m = markers(pr);
156
+ // Marker lands on the text block, not the thinking block.
157
+ const finalMarks = m.messages.filter(([mi]) => mi === last(pr));
158
+ expect(finalMarks).toHaveLength(1);
159
+ const [, bi] = finalMarks[0];
160
+ expect(pr.messages[last(pr)].content[bi].type).toBe('text');
161
+ });
162
+ it('stands down entirely when the normalizer synthesized a [pending] tool_result', () => {
163
+ // Trailing orphan tool_use → normalizer synthesizes its [pending]
164
+ // result, whose bytes change when the real result lands.
165
+ const messages = [user('go', true), assistantToolCall('t1')];
166
+ const pr = build(makeRequest(), messages, true);
167
+ // Only the kickoff marker; nothing floated at or past the synthetic.
168
+ expect(markers(pr).messages).toEqual([[0, 0]]);
169
+ });
170
+ it('respects request-level opt-out', () => {
171
+ const pr = build(makeRequest({ floatingCacheMarker: false }), turn(1), true);
172
+ expect(markers(pr).messages).toEqual([[0, 0]]);
173
+ });
174
+ it('emits no cache_control at all when promptCaching is off', () => {
175
+ const pr = build(makeRequest({ promptCaching: false }), turn(1), true);
176
+ const m = markers(pr);
177
+ expect(m.messages).toEqual([]);
178
+ expect(m.onTools).toBe(false);
179
+ expect(m.onSystem).toBe(false);
180
+ });
181
+ it('counts pre-marked system blocks against the residual budget (never exceeds 4 on the wire)', () => {
182
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
183
+ // System is a block array with one block already carrying cache_control —
184
+ // invisible to the running message tally, but a real wire marker.
185
+ const premarkedSystem = [
186
+ { type: 'text', text: 'You are a test agent.', cache_control: { type: 'ephemeral' } },
187
+ { type: 'text', text: 'Addendum.' },
188
+ ];
189
+ // 3 message markers + pre-marked system = 4 on the wire: no room to float.
190
+ const full = turn(2);
191
+ for (const msg of full) {
192
+ if (msg.content[0].type === 'tool_result')
193
+ msg.cacheBreakpoint = true;
194
+ }
195
+ const prFull = build(makeRequest({ system: premarkedSystem }), full, true);
196
+ expect(totalMarkers(prFull)).toBe(4);
197
+ expect(warn).toHaveBeenCalledTimes(1);
198
+ // 2 message markers + pre-marked system = 3: exactly one slot left — the
199
+ // float takes the newest message and stops.
200
+ const partial = turn(2);
201
+ partial[2].cacheBreakpoint = true; // first round's results envelope
202
+ const prPartial = build(makeRequest({ system: premarkedSystem }), partial, true);
203
+ expect(totalMarkers(prPartial)).toBe(4);
204
+ expect(markers(prPartial).messages).toContainEqual([last(prPartial), 0]);
205
+ });
206
+ it('counts an overlapping block-level + message-level marker once (one physical marker)', () => {
207
+ // A stale block-level cache_control on the very block the message's
208
+ // cacheBreakpoint lands on is ONE wire marker, not two. Two such
209
+ // messages must leave residuum 2, not 0.
210
+ const staleMarked = (t) => ({
211
+ participant: 'User',
212
+ content: [{ type: 'text', text: t, cache_control: { type: 'ephemeral' } }],
213
+ cacheBreakpoint: true,
214
+ });
215
+ // Results envelope whose message-level breakpoint lands on a text block
216
+ // that already carries stale cache_control — tool pairing stays intact.
217
+ const overlapResults = (id) => ({
218
+ participant: 'User',
219
+ content: [
220
+ { type: 'tool_result', toolUseId: id, content: 'ok' },
221
+ { type: 'text', text: 'operator note', cache_control: { type: 'ephemeral' } },
222
+ ],
223
+ cacheBreakpoint: true,
224
+ });
225
+ const messages = [staleMarked('do the thing'),
226
+ assistantToolCall('t1'), overlapResults('t1'),
227
+ assistantToolCall('t2'), toolResults('t2')];
228
+ // 2 physical wire markers (the old tally saw 4 and withheld everything).
229
+ const pr = build(makeRequest(), messages, true);
230
+ const m = markers(pr);
231
+ // Float not withheld: newest message marked; previous endpoint already
232
+ // carries its own marker (dedupe, no slot spent).
233
+ expect(m.messages).toContainEqual([last(pr), 0]);
234
+ expect(totalMarkers(pr)).toBeLessThanOrEqual(4);
235
+ });
236
+ it('floated markers carry the request cacheTtl', () => {
237
+ const pr = build(makeRequest(), turn(1), true);
238
+ const [mi, bi] = markers(pr).messages.find(([i]) => i === last(pr));
239
+ expect(pr.messages[mi].content[bi].cache_control).toEqual({ type: 'ephemeral', ttl: '1h' });
240
+ });
241
+ });
242
+ //# sourceMappingURL=floating-cache-marker.test.js.map