pi-l1-cache 1.2.1 → 1.4.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/README.md +131 -104
- package/fix-l1-cache.cjs +405 -0
- package/package.json +18 -7
- package/src/index.test.ts +462 -84
- package/src/index.ts +269 -214
package/README.md
CHANGED
|
@@ -1,177 +1,204 @@
|
|
|
1
1
|
# pi-l1-cache
|
|
2
2
|
|
|
3
|
-
> **L1 In-Memory Cache Extension for pi** —
|
|
3
|
+
> **L1 In-Memory Cache Extension for pi** — with disk persistence, response replay, and working capture
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://pi.dev/packages)
|
|
7
|
+
[](https://www.npmjs.com/package/pi-l1-cache)
|
|
7
8
|
|
|
8
|
-
A high-performance, production-ready **L1 (in-memory) cache** extension for the [pi coding agent](https://pi.dev).
|
|
9
|
+
A high-performance, production-ready **L1 (in-memory + disk) cache** extension for the [pi coding agent](https://pi.dev). Unlike the stock API which lacks response capture, this implementation uses a **pi-core patch** to enable full caching with replay.
|
|
10
|
+
|
|
11
|
+
## ⚡ Performance
|
|
12
|
+
|
|
13
|
+
| Scenario | Cold | Warm (replayed) | Speedup |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| Simple prompt | 3.2s | **1.9s** | ~1.7× |
|
|
16
|
+
| **Tool-calling agent loop** | 13.1s | **2.5s** | **5.2×** |
|
|
9
17
|
|
|
10
18
|
## Features
|
|
11
19
|
|
|
12
20
|
| Feature | Description |
|
|
13
21
|
|---------|-------------|
|
|
22
|
+
| **Response replay** | Cached chunks are fed through pi-ai's normal consume path — parsing, usage, tool-calls, stop-reason all identical |
|
|
23
|
+
| **Disk persistence** | Survives across `pi -p` process restarts (`~/.pi/agent/cache/l1-cache/`) |
|
|
14
24
|
| **~138µs overhead** | End-to-end per-request cost (measured, incl. key hashing + JSON) |
|
|
15
25
|
| **Fast key hash** | FNV-1a (~1.5µs/op) — no per-request CPU probe on the hot path |
|
|
16
26
|
| **Memory cap** | Hard limit (20MB default) prevents RAM bloat |
|
|
17
27
|
| **Auto-eviction** | LRU-style cleanup when limits reached |
|
|
18
|
-
| **CPU-aware** | Auto-disables when CPU > 95% (checked once at startup) |
|
|
19
28
|
| **TTL-based** | 1-hour default expiry for cached entries |
|
|
20
|
-
| **
|
|
29
|
+
| **Coalescing** | Adjacent content/reasoning_content deltas merged to shrink storage |
|
|
21
30
|
|
|
22
31
|
## Architecture
|
|
23
32
|
|
|
24
33
|
```
|
|
25
|
-
pi → [L1: RAM Map
|
|
26
|
-
~138µs
|
|
34
|
+
pi → [L1: RAM Map + disk] → Provider
|
|
35
|
+
~138µs lookup 1-3s
|
|
27
36
|
```
|
|
28
37
|
|
|
38
|
+
The extension uses a **pi-core patch** (`fix-l1-cache.cjs`) to add two capabilities the stock extension API lacks:
|
|
39
|
+
|
|
40
|
+
> **Bundle era (pi ≥ 0.84):** pi runs from the esbuild bundle (`dist/bundle/cli.js`), so `fix-l1-cache.cjs` patches `dist/bundle/chunks/*` (minified, anchor-matched — verified against 0.86.0). Pre-bundle pi (< 0.84) is patched in the readable pi-ai sources. The patch is idempotent and bundle-era misses degrade to pass-through rather than failing an install.
|
|
41
|
+
|
|
42
|
+
1. **REPLAY** — an extension may serve a cached response by returning params with `__piL1Replay: [chunks]` from `before_provider_request`; pi-ai feeds the cached chunks through the normal consume path and never contacts the provider.
|
|
43
|
+
|
|
44
|
+
2. **CAPTURE** — after a successful completion, pi-ai calls `options.onStreamComplete(allChunks, requestParams)`; `sdk.js` forwards them to extensions as a `provider_stream_complete` event so the cache can store the response.
|
|
45
|
+
|
|
29
46
|
## Installation
|
|
30
47
|
|
|
48
|
+
### Option 1: From npm (recommended)
|
|
49
|
+
|
|
31
50
|
```bash
|
|
32
|
-
# From npm (recommended)
|
|
33
51
|
pi install npm:pi-l1-cache
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This installs the extension AND automatically applies the `fix-l1-cache.cjs` pi-core patch.
|
|
55
|
+
|
|
56
|
+
### Option 2: From GitHub
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pi install git:github.com:tobias-weiss-ai-xr/pi-l1-cache@main
|
|
60
|
+
```
|
|
34
61
|
|
|
35
|
-
|
|
36
|
-
pi install git:github.com/tobias-weiss-ai-xr/pi-l1-cache@main
|
|
62
|
+
### Option 3: From local clone
|
|
37
63
|
|
|
38
|
-
|
|
64
|
+
```bash
|
|
39
65
|
pi install /path/to/pi-l1-cache
|
|
40
66
|
```
|
|
41
67
|
|
|
42
68
|
Then **restart pi** — the extension auto-loads.
|
|
43
69
|
|
|
44
|
-
##
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
### Environment Variables
|
|
45
73
|
|
|
46
74
|
```bash
|
|
47
|
-
#
|
|
48
|
-
|
|
75
|
+
# Enable/disable
|
|
76
|
+
L1_CACHE_ENABLED=true
|
|
49
77
|
|
|
50
|
-
#
|
|
51
|
-
|
|
78
|
+
# Max entries (default: 200)
|
|
79
|
+
L1_CACHE_MAX_ENTRIES=200
|
|
52
80
|
|
|
53
|
-
#
|
|
54
|
-
|
|
81
|
+
# Max memory in MB (default: 20)
|
|
82
|
+
L1_CACHE_MAX_MB=20
|
|
83
|
+
|
|
84
|
+
# TTL in seconds (default: 3600)
|
|
85
|
+
L1_CACHE_TTL=3600
|
|
55
86
|
|
|
56
|
-
#
|
|
57
|
-
|
|
87
|
+
# Log stats on each hit/miss (default: false)
|
|
88
|
+
L1_CACHE_LOG=true
|
|
89
|
+
```
|
|
58
90
|
|
|
59
|
-
|
|
60
|
-
|
|
91
|
+
### Settings (in `~/.pi/settings.json`)
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"extensions": {
|
|
96
|
+
"l1-cache": {
|
|
97
|
+
"enabled": true,
|
|
98
|
+
"maxEntries": 200,
|
|
99
|
+
"maxMemoryBytes": 20971520,
|
|
100
|
+
"ttlSeconds": 3600,
|
|
101
|
+
"persist": true,
|
|
102
|
+
"logStats": false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
61
106
|
```
|
|
62
107
|
|
|
63
|
-
|
|
108
|
+
## Usage
|
|
64
109
|
|
|
110
|
+
### Show cache stats
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
/l1-cache
|
|
65
114
|
```
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
Hits: 23 | Misses: 70 | Evictions: 5
|
|
71
|
-
Hit rate: 24.7%
|
|
72
|
-
Init CPU: 45.2% (ok)
|
|
73
|
-
Last cleanup: 2025-08-22T10:30:00.000Z
|
|
115
|
+
|
|
116
|
+
Output:
|
|
117
|
+
```
|
|
118
|
+
L1 cache: 16 entries, 43.2KB | hits 12 (replays 12), misses 4, writes 4, evictions 0 | dir: C:/Users/Tobias/.pi/agent/cache/l1-cache
|
|
74
119
|
```
|
|
75
120
|
|
|
76
|
-
|
|
121
|
+
### Clear cache
|
|
77
122
|
|
|
78
|
-
|
|
123
|
+
```bash
|
|
124
|
+
/l1-cache clear
|
|
125
|
+
```
|
|
79
126
|
|
|
80
|
-
|
|
127
|
+
Cleared: memory + disk (all `.json` files in cache dir).
|
|
81
128
|
|
|
82
|
-
|
|
83
|
-
|----------|---------|-------------|
|
|
84
|
-
| `L1_CACHE_ENABLED` | `true` | Master enable/disable |
|
|
85
|
-
| `L1_CACHE_MAX_ENTRIES` | `200` | Maximum number of cache entries |
|
|
86
|
-
| `L1_CACHE_MAX_MB` | `20` | Maximum memory in MB |
|
|
87
|
-
| `L1_CACHE_TTL` | `3600` | TTL in seconds (1 hour) |
|
|
88
|
-
| `L1_CACHE_LOG` | `false` | Enable debug logging |
|
|
129
|
+
## Key Semantics
|
|
89
130
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
131
|
+
The cache key is a stable hash of:
|
|
132
|
+
- `model`
|
|
133
|
+
- `messages` (full conversation history)
|
|
134
|
+
- `tools`
|
|
135
|
+
- `tool_choice`
|
|
136
|
+
- `temperature`, `top_p`
|
|
137
|
+
- `reasoning_effort`, `thinking`
|
|
138
|
+
- `max_completion_tokens`, `max_tokens`
|
|
94
139
|
|
|
95
|
-
|
|
96
|
-
L1_CACHE_MAX_MB=50 L1_CACHE_TTL=1800 pi
|
|
97
|
-
```
|
|
140
|
+
**Volatile fields are EXCLUDED:** `prompt_cache_key`, `prompt_cache_retention`, `stream`, `stream_options`, `store`, `sessionId`.
|
|
98
141
|
|
|
99
|
-
|
|
142
|
+
This means:
|
|
143
|
+
- ✅ Cross-run hits possible (same prompt, same cwd, same model)
|
|
144
|
+
- ✅ Tool-calling agent loops fully replayed (tool results are part of messages)
|
|
145
|
+
- ❌ Different conversation history = different key (expected)
|
|
146
|
+
- ❌ Session-derived fields don't break cross-run hits
|
|
100
147
|
|
|
101
|
-
|
|
148
|
+
## Gotchas
|
|
102
149
|
|
|
103
|
-
|
|
104
|
-
const DEFAULTS: Settings = {
|
|
105
|
-
enabled: true,
|
|
106
|
-
maxEntries: 200,
|
|
107
|
-
maxMemoryBytes: 20 * 1024 * 1024, // 20MB
|
|
108
|
-
ttlSeconds: 3600, // 1 hour
|
|
109
|
-
cpuThreshold: 95, // disable if CPU > 95%
|
|
110
|
-
logStats: false,
|
|
111
|
-
}
|
|
112
|
-
```
|
|
150
|
+
1. **Replays are canned** — identical input returns the stored response verbatim. For fresh answers, use `/l1-cache clear`.
|
|
113
151
|
|
|
114
|
-
|
|
152
|
+
2. **Print mode** (`pi -p`) reads entire stdin as ONE prompt — you cannot test two identical requests in one process this way.
|
|
115
153
|
|
|
116
|
-
|
|
117
|
-
- **One thing, done well** — Caching, and only caching
|
|
118
|
-
- **Do not rely on external services** — Pure in-memory, no Redis
|
|
119
|
-
- **Graceful degradation** — Works even on constrained systems
|
|
120
|
-
- **Zero dependencies** — Single TypeScript file
|
|
154
|
+
3. **llm-timestamp.js** does NOT pollute provider messages (it only appends display-level `message_end` entries), so keys are stable across runs.
|
|
121
155
|
|
|
122
|
-
|
|
123
|
-
1. **Cheap key hashing** (FNV-1a, ~1.5µs/op). Note: Node's native SHA-256 (OpenSSL) is marginally *faster* than a JS FNV loop — hashing is a rounding error next to `JSON.stringify(messages)`, and both are >1000× below provider latency. The real win is *not* a faster hash, it is skipping the provider call.
|
|
124
|
-
2. **L1 only** — avoid disk I/O in hot path
|
|
125
|
-
3. **Batch eviction** — remove 10-20% at a time, not one-by-one
|
|
126
|
-
4. **Periodic cleanup** — async, non-blocking garbage collection
|
|
127
|
-
5. **Single CPU check** — at startup only, not per-request
|
|
156
|
+
4. **Replayed responses carry original responseId/usage** — accurate since input identical.
|
|
128
157
|
|
|
129
|
-
##
|
|
158
|
+
## Troubleshooting
|
|
130
159
|
|
|
131
|
-
|
|
132
|
-
# Run all tests
|
|
133
|
-
npm test
|
|
160
|
+
### "fix-reasoning-content.js: layout changed"
|
|
134
161
|
|
|
135
|
-
|
|
136
|
-
npm run test:watch
|
|
162
|
+
The `fix-l1-cache.cjs` patch modifies the same file as `fix-reasoning-content.js`. The wrapper's postinstall chain handles this correctly — `fix-reasoning-content.js` now recognizes its work via marker even after the replay branch is added.
|
|
137
163
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
164
|
+
If you see this error:
|
|
165
|
+
1. Ensure you're running the full postinstall chain (not individual scripts)
|
|
166
|
+
2. Check that `fix-reasoning-content.js` has the marker-based detection (v1.2.3+)
|
|
167
|
+
3. Verify postinstall order: `fix-reasoning-content.js` BEFORE `fix-l1-cache.cjs`
|
|
141
168
|
|
|
142
|
-
|
|
143
|
-
- Hash consistency & collision resistance
|
|
144
|
-
- Size estimation for various data types
|
|
145
|
-
- LRU eviction behavior (entry count + memory)
|
|
146
|
-
- State management & reset
|
|
147
|
-
- Settings override
|
|
169
|
+
### Cache never hits
|
|
148
170
|
|
|
149
|
-
|
|
171
|
+
Check:
|
|
172
|
+
1. `L1_CACHE_LOG=true` to see HIT/MISS/STORED logs
|
|
173
|
+
2. Prompt is byte-identical (including system prompt, tools, cwd context)
|
|
174
|
+
3. Disk persistence is enabled (`persist: true`)
|
|
175
|
+
4. TTL hasn't expired (default 1h)
|
|
150
176
|
|
|
151
|
-
|
|
177
|
+
## Development
|
|
152
178
|
|
|
153
|
-
|
|
154
|
-
|---|---|
|
|
155
|
-
| `fastHash` (FNV-1a) standalone | ~670k ops/s — 1.49µs/op |
|
|
156
|
-
| Node native `sha256` (for reference) | ~850k ops/s — 1.18µs/op |
|
|
157
|
-
| Full put: hash + map set + size check | ~249k ops/s — 4.0µs/op |
|
|
158
|
-
| Interceptor path (hit *and* miss, 15-msg history) | ~138µs/request |
|
|
159
|
-
| Cache hit vs uncached provider round-trip (1–3s) | ~7,000–20,000× wall-clock |
|
|
160
|
-
| Key uniqueness over 50k synthetic keys | 50,000/50,000 (no collisions) |
|
|
161
|
-
| Eviction under 200-slot cap, 20k inserts | cap held; 19,800 evicted |
|
|
179
|
+
### Testing
|
|
162
180
|
|
|
163
|
-
|
|
181
|
+
```bash
|
|
182
|
+
# Run unit tests
|
|
183
|
+
npm test
|
|
184
|
+
|
|
185
|
+
# Run the extension manually
|
|
186
|
+
npx tsx src/index.ts
|
|
187
|
+
```
|
|
164
188
|
|
|
165
|
-
|
|
189
|
+
### Benchmark
|
|
166
190
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
191
|
+
```bash
|
|
192
|
+
# Measure cold vs warm times
|
|
193
|
+
echo "What is 7*6?" | time pi -p "test" # cold
|
|
194
|
+
echo "What is 7*6?" | time pi -p "test" # warm (should be ~1.7× faster)
|
|
195
|
+
```
|
|
170
196
|
|
|
171
197
|
## License
|
|
172
198
|
|
|
173
199
|
MIT — see [LICENSE](LICENSE)
|
|
174
200
|
|
|
175
|
-
##
|
|
201
|
+
## Contact
|
|
176
202
|
|
|
177
|
-
|
|
203
|
+
- Issues: https://github.com/tobias-weiss-ai-xr/pi-l1-cache/issues
|
|
204
|
+
- Email: info@graphwiz.ai
|