@chroxify/eunoe 0.1.0 → 1.0.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.
Files changed (43) hide show
  1. package/README.md +315 -236
  2. package/package.json +3 -2
  3. package/src/cli/args.ts +1 -1
  4. package/src/cli/commands/index.ts +16 -2
  5. package/src/cli/commands/mode.ts +53 -17
  6. package/src/cli/commands/serve.ts +2 -1
  7. package/src/cli/commands/settings.ts +100 -0
  8. package/src/cli/commands/start.ts +64 -0
  9. package/src/cli/commands/status.ts +50 -16
  10. package/src/cli/commands/stop.ts +33 -11
  11. package/src/cli/commands/uninstall.ts +11 -9
  12. package/src/cli/constants.ts +28 -5
  13. package/src/cli/help.ts +2 -2
  14. package/src/cli/launchd.ts +8 -3
  15. package/src/cli/prompt.ts +91 -0
  16. package/src/cli/service.ts +33 -0
  17. package/src/cli/sessions.ts +25 -0
  18. package/src/cli/stats.ts +73 -0
  19. package/src/cli/types.ts +1 -0
  20. package/src/cli/ui.ts +15 -0
  21. package/src/config/constants.ts +15 -3
  22. package/src/config/index.ts +55 -6
  23. package/src/config/types.ts +34 -5
  24. package/src/context/constants.ts +35 -0
  25. package/src/context/content.ts +23 -1
  26. package/src/context/evidence.ts +92 -0
  27. package/src/context/recall.ts +155 -0
  28. package/src/context/rerank.ts +44 -0
  29. package/src/context/strategies.ts +6 -6
  30. package/src/context/turns.ts +19 -14
  31. package/src/context/types.ts +43 -1
  32. package/src/proxy/canonical.ts +32 -0
  33. package/src/proxy/compaction.ts +29 -12
  34. package/src/proxy/constants.ts +10 -0
  35. package/src/proxy/fingerprint.ts +15 -0
  36. package/src/proxy/rewrite.ts +84 -10
  37. package/src/proxy/server.ts +30 -12
  38. package/src/proxy/sessions.ts +46 -0
  39. package/src/proxy/types.ts +24 -0
  40. package/src/transcript/constants.ts +0 -1
  41. package/src/transcript/locate.ts +1 -1
  42. package/src/transcript/messages.ts +29 -0
  43. package/src/cli/commands/install.ts +0 -44
package/README.md CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  **Context management for Claude Code that never forgets what matters.**
8
8
 
9
- A local proxy that replaces Claude Code's context compaction. No summary call,
10
- no two-minute pause, no agent that wakes up having forgotten what you told it.
9
+ A local proxy that keeps long Claude Code sessions sharp. Smaller context,
10
+ nothing lost, no pause, no restart.
11
11
 
12
12
  </div>
13
13
 
@@ -19,310 +19,389 @@ no two-minute pause, no agent that wakes up having forgotten what you told it.
19
19
 
20
20
  ## The problem
21
21
 
22
- Fill up a long Claude Code session and it compacts. The agent writes a summary
23
- of everything so far, throws the conversation away, and continues from the
24
- summary.
22
+ Every long Claude Code session eventually turns dumb on you, and it happens
23
+ well before the context window is full. The agent starts forgetting rules you
24
+ gave it an hour ago, re-reading files it already read, and asking about
25
+ decisions you made together. When compaction finally kicks in it makes things
26
+ worse, because the whole conversation gets replaced by a summary and anything
27
+ the summary left out is gone for good.
25
28
 
26
- You can feel it the moment it happens.
29
+ At that point the only fix is a fresh session, which means explaining the
30
+ project again, pasting the rules again, and hoping it gets further this time.
27
31
 
28
- ```
29
- you (turn 6) from now on, deploy to staging yourself, don't ask me
30
- ...
31
- you (turn 41) ok ship it
32
- agent Here's the deploy command — want me to run it?
33
- ```
34
-
35
- The instruction was five turns old and three paragraphs into a 21,000-character
36
- summary. So were the exact version ID from the last deploy, the error you spent
37
- two turns tracking down, and the approach you already ruled out.
32
+ The reason is that every file the agent ever opened and every command it ever
33
+ ran stays in the prompt, so two hundred turns in the model is still reading a
34
+ log from the first hour on every request, with the thing you said five turns
35
+ ago buried somewhere in the middle of it.
38
36
 
39
- Compaction keeps the gist. The gist is not what you need.
40
-
41
- It also costs real time. A measured compaction at 922k tokens: **the full
42
- context read for the summary call, 2½ minutes of generation**, then a brand-new
43
- prefix written to cache anyway.
37
+ | | Claude Code | eunoe |
38
+ |---|---|---|
39
+ | **context** | grows with every tool call until it fills the window | finished turns fold to your message, the output lines that mattered and the reply; turns in progress go out whole |
40
+ | **at the limit** | a two-minute summary call replaces the whole conversation | never reached |
41
+ | **old details** | gone unless the summary kept them | come back the moment a message is about them |
42
+ | **your instructions** | survive if the summary mentions them | kept word for word |
43
+ | **the session** | one window, then you start over | as sharp at turn three hundred as at turn three |
44
+
45
+ ## Results
46
+
47
+ Measured on 24 held-out sessions of 90 to 120 turns with 600 facts planted
48
+ at known depths, such as a value a command printed once, an error, an
49
+ instruction that was later revised or a decision with its rejected options,
50
+ and scored by exact match with no judge involved.
51
+
52
+ | | facts remembered | rules followed without a reminder | prompt size when asked | tokens per session | transcript lookups per session | time per session | said "I don't know" | answered wrong |
53
+ |---|---|---|---|---|---|---|---|---|
54
+ | Claude Code's compaction | 70% | 92% | 70k | 741k | 10.1 | 53s | 172 of 600 | 7 of 600 |
55
+ | Claude Code, told a transcript exists | 99% | 92% | 71k | 1,315k | 23.0 | 88s | 3 of 600 | 6 of 600 |
56
+ | keeping the whole history | 100% | 90% | 411k | 815k | 0 | 67s | 0 of 600 | 3 of 600 |
57
+ | eunoe, folding only | 99% | 92% | 118k | 490k | 2.7 | 40s | 4 of 600 | 5 of 600 |
58
+ | **eunoe** | **99%** | **95%** | **110k** | **305k** | **0.1** | **39s** | **1 of 600** | **3 of 600** |
59
+
60
+ *Facts remembered* is how many of the planted facts the agent could still
61
+ answer correctly after the cut. *Rules followed without a reminder* is how
62
+ often it applied an instruction from earlier in the session when the task
63
+ called for it, without being told again. *Prompt size* is the context the
64
+ model was reading when the question came, while *tokens per session* adds up
65
+ every request it took to get through the questions, so it grows whenever the
66
+ agent has to go and look something up.
67
+
68
+ That is **29 points more recall than Claude Code's compaction** (95% CI +17
69
+ to +42), better on 12 sessions and worse on none, with 3 wrong answers out
70
+ of 600, and the same margin holds on 27 cut points taken from real sessions.
71
+ eunoe matches keeping the whole history at a quarter of the context and 40%
72
+ of the tokens.
73
+
74
+ The surprising part is not that eunoe remembers more, it is how. Telling
75
+ Claude Code that a transcript exists gets it to 99% too, but it has to grep
76
+ for the answer 23 times a session to do it, which is why it burns four times
77
+ the tokens. eunoe reaches the same 99% with about one lookup every ten
78
+ sessions, because the answer is already in the prompt when the question
79
+ arrives, and it is faster than keeping the whole history even though that
80
+ arm never searches at all. The two eunoe rows differ only in whether the
81
+ exact lines of old output are kept and brought back, which is the part of
82
+ the design explained below.
83
+
84
+ The method was chosen on a separate development split and frozen before the
85
+ test split ran. The suites, the pre-registered spec, every per-arm table and
86
+ the limits of the study (one developer's repositories, one answering model,
87
+ one run per arm) are in [`bench/`](bench/README.md).
44
88
 
45
89
  ## Install
46
90
 
47
91
  ```sh
48
- bun add -g @chroxify/eunoe # puts `eunoe` on your PATH
49
- eunoe install # background proxy + ANTHROPIC_BASE_URL in ~/.claude/settings.json
92
+ bun add -g @chroxify/eunoe # puts `eunoe` and the short `ee` on your PATH
93
+ eunoe start # sets up the background proxy, routes Claude Code through it
50
94
  ```
51
95
 
52
- Then use Claude Code exactly as before. New sessions go through eunoe; restart
53
- any that are already running. `eunoe uninstall` reverses it.
96
+ Then use Claude Code exactly as before. New sessions go through eunoe, and
97
+ sessions that were already running pick it up when you restart them.
98
+ `eunoe stop` turns it off again.
99
+
100
+ Requires [Bun](https://bun.sh). On macOS `start` sets up a launchd agent that
101
+ keeps the proxy alive; on other platforms keep `eunoe serve` running however
102
+ you like and everything else works the same.
54
103
 
55
- Requires [Bun](https://bun.sh). `install` sets up a launchd agent on macOS; on
56
- other platforms, keep `eunoe serve` running however you like (systemd, a
57
- terminal) and the rest works the same.
104
+ ## How it works
58
105
 
59
- ## What it does instead
106
+ eunoe does three things, and each one was measured on its own before it was
107
+ kept.
60
108
 
61
- Nothing, until the context is nearly full. Every request up to that point goes
62
- upstream byte for byte as Claude Code built it.
109
+ ### Folding
63
110
 
64
- At the limit, eunoe rewrites the conversation once. Your first message stays.
65
- Every turn after it collapses to **what you asked and what the agent answered**.
66
- The turn in progress is kept whole, down to the last tool call.
111
+ A **turn** is one message from you and everything the agent did until your
112
+ next one. Once a turn is finished, most of what it holds is tool output the
113
+ agent needed in the moment and will never read again.
114
+
115
+ A finished turn, as Claude Code keeps it (about 9,000 tokens):
67
116
 
68
117
  ```
69
- WHAT CLAUDE CODE HOLDS (~900k) WHAT eunoe SENDS
70
- ────────────────────────────── ────────────────────────────────────────
71
- turn 1 first message turn 1 first message, byte for byte
72
- + the agent's work + reply ┌──────────────────────────────────────┐
73
- turn 2 prompt, 14 tool calls, reply │ <context-compacted> │
74
- turn 3 prompt, 6 tool calls, reply │ Tool calls before this point were │
75
- ⋮ │ removed. Earlier turns follow as │
76
- turn 38 prompt, 9 tool calls ✋ │ your message + the final reply. │
77
- (you interrupted mid-work) │ Instructions you gave still apply. │
78
- turn 39 prompt, 3 tool calls, reply │ Exact details → search the │
79
- turn 40 prompt, 22 tool calls ← LIMIT │ transcript: <how> │
80
- └──────────────────────────────────────┘
81
- turn 2 your message → final reply
82
- turn 3 your message → final reply
83
- ⋮ every turn, ~580 tokens each
84
- turn 38 your message → [interrupted:
85
- last words + its tool calls]
86
- turn 39 your message → final reply
87
- turn 40 your message + every tool call
88
- and result, exactly as it was
89
- turn 41+ grows normally until the next cut
118
+ [you] fix the failing lease test
119
+
120
+ [agent] I'll look at the test first.
121
+ [tool] Read src/lease.test.ts
122
+ ... 140 lines of file ...
123
+ [tool] Bash bun test
124
+ ... 80 lines of output ...
125
+
126
+ [agent] The expiry check is off by one.
127
+ [tool] Edit src/lease.ts
128
+ [tool] Bash bun test
129
+ ... 80 lines of output ...
130
+
131
+ [agent] Fixed the off-by-one in renew(): the expiry check used <= instead
132
+ of <. All 4 tests pass. The 6011 code comes from LEASE_EXPIRED in
133
+ src/errors.ts.
90
134
  ```
91
135
 
92
- **Every turn survives.** A message plus its final reply averages ~580 tokens, so
93
- a hundred turns is ~60k. There is no reason to throw them away. This is what
94
- keeps *"what did we do about the Stripe thing"* and *"you told me to deploy it
95
- yourself"* answerable after a cut.
136
+ The same turn after eunoe folds it (about 700 tokens):
96
137
 
97
- **The first message is kept verbatim.** Claude Code packs `CLAUDE.md`, your git
98
- status, your identity and the skill list into it. Dropping it drops your
99
- instructions. Its hash also travels in a billing header, so eunoe never edits it.
138
+ ```
139
+ [you] fix the failing lease test
100
140
 
101
- **The turn in progress is untouched.** The agent is mid-work: it needs the actual
102
- tool calls, their output and its own thinking blocks to carry on. If you
103
- interrupted it, the interruption marker is right there too, so it resumes from
104
- where it stopped rather than starting the turn over.
141
+ [evidence] Bash bun test
142
+ FAIL lease renews after expiry
143
+ expected 6011, got undefined
144
+ 3 pass, 1 fail
105
145
 
106
- ## How it plugs in
146
+ [agent] Fixed the off-by-one in renew(): the expiry check used <= instead
147
+ of <. All 4 tests pass. The 6011 code comes from LEASE_EXPIRED in
148
+ src/errors.ts.
149
+ ```
150
+
151
+ The agent already writes a usable summary of every turn in its final reply,
152
+ so folding keeps that reply, your message and the evidence described below,
153
+ and drops everything else. A hundred folded turns come to about 70k tokens,
154
+ which is why eunoe never has to throw a turn away.
155
+
156
+ The timing is what makes this cheap. The last 3 turns are always sent whole,
157
+ down to the last tool call, and older turns stay whole too until the detail
158
+ in them passes a budget of 100k tokens. At that point every turn except
159
+ those 3 folds in one step.
107
160
 
108
161
  ```
109
- Claude CLI ─┐
110
- Desktop app ┼──► eunoe (127.0.0.1:8788) ──► api.anthropic.com
111
- Agent SDK ──┘ rewrites /v1/messages
112
- passes everything else straight through
162
+ turn 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
163
+ -----------------------------------------------------------------------
164
+ t=10 W W W W W W W W W L budget not reached
165
+ t=20 W W W W W W W W W W W W W W W W W W W L
166
+ t=21 F F F F F F F F F F F F F F F F F F W W L <- fold
167
+ t=24 F F F F F F F F F F F F F F F F F F W W W W W L
168
+
169
+ W whole F folded L the live turn
113
170
  ```
114
171
 
115
- Claude Code honours `ANTHROPIC_BASE_URL` from its `settings.json`, so every
116
- harness built on it works with no code changes. Auth headers are forwarded
117
- untouched — your existing login keeps working, subscription or API key.
172
+ It happens in a step rather than a slide because of the prompt cache. The
173
+ cache is a prefix match, so changing anything in the middle rewrites
174
+ everything after it at 1.25 times the price of a read. Folding one turn per
175
+ message would rewrite the cache on every message, whereas folding a budget
176
+ at a time writes the new prefix once and lets every request until the next
177
+ step read it at a tenth of the price.
118
178
 
119
- Claude Code still keeps the whole conversation in memory and on disk at
120
- `~/.claude/projects/<project>/<session>.jsonl`. eunoe only changes what goes out
121
- on the wire. That untouched file is the transcript the agent gets pointed at.
179
+ The first message is always kept verbatim, because Claude Code packs
180
+ `CLAUDE.md`, your git status and your skills into it and its hash travels in
181
+ a billing header. The live turns are untouched too, thinking blocks and
182
+ interruptions included. If a turn was interrupted before the agent replied,
183
+ the fold keeps its last words and the list of what it ran instead.
122
184
 
123
- ## Modes
124
-
125
- | mode | before the limit | at the limit |
126
- |---|---|---|
127
- | **`compact`** (default) | everything, unchanged | the cut, above |
128
- | **`trim`** | every finished turn is already just your message + the final reply | rarely reached |
129
- | **`off`** | passthrough | Claude Code's own compaction |
185
+ ### Evidence
130
186
 
131
- `trim` is the aggressive option: tool output leaves context the moment a turn
132
- ends, so a long session never grows past a few tens of thousands of tokens. It
133
- is noticeably cheaper and, in benchmarks, no less accurate — but the agent has
134
- to search its transcript more often, so turns start slower.
187
+ Folding on its own would lose the exact values in tool output, like the
188
+ deployment id, the error code or the test count, so when a turn folds each
189
+ command's output is scored line by line and the lines worth keeping are
190
+ quoted verbatim under the command that produced them.
135
191
 
136
- ```sh
137
- eunoe mode compact # or trim, or off
192
+ ```
193
+ score line
194
+ ----- ------------------------------------------------------------
195
+ +3 FAIL lease renews after expiry error words and codes
196
+ +3 Deployment ID: dpl_qgthe6u4p7 ids, hashes, URLs
197
+ +2 3 pass, 1 fail test and diff summaries
198
+ +2 ERR_LEASE_6011: lease expired error codes
199
+ +2 took 412 ms durations, sizes, versions
200
+ +1 status: applied key: value
201
+ 0 Compiling 14 modules... dropped
202
+ 0 > lease@1.4.0 test dropped
138
203
  ```
139
204
 
140
- Whichever is on owns compaction: eunoe disables Claude Code's auto-compact while
141
- active and hands it back on `off` or `uninstall`.
205
+ Every command keeps its best lines, and the highest scorers across the turn
206
+ fill a budget of about 4,000 characters, which is a few hundred tokens. The
207
+ cap exists because a long test or build log would otherwise fold to
208
+ something bigger than the turn it came from, and the size was chosen on the
209
+ development split, where it kept 97% of the planted values. Commands whose
210
+ output can simply be run again, such as `Read`, `Grep`, `Glob`, `Edit` and
211
+ `Write`, are skipped entirely.
142
212
 
143
- ## What counts as a final reply
213
+ ### Recall
144
214
 
145
- A **turn** starts with a message you sent — text or an image, not a tool result —
146
- and runs until your next one.
215
+ Each time you send a message, before the agent responds, eunoe uses it as a
216
+ search query over every tool output that is no longer in context and
217
+ everything the agent said while working. Matching excerpts are quoted
218
+ exactly and appended to your message, oldest first.
147
219
 
148
- Its **final reply** is the last assistant message of that turn, if the turn ended
149
- on text with no tool call in it. Only the text is kept; thinking is dropped.
220
+ ```
221
+ you (turn 71) deploy is failing with ERR_LEASE_6011 again
150
222
 
151
- Across 18,966 real turns:
223
+ eunoe appends:
152
224
 
153
- | how the turn ended | share | what eunoe keeps |
154
- |---|---|---|
155
- | on a text reply | 73% | that reply (median 1.9k chars; 57% name a file path) |
156
- | still running | 17% | nothing — it's the live turn, never reduced |
157
- | interrupted or steered | 10% | last words + the list of tool calls |
225
+ <recalled-context>
226
+ [turn 12] Bash `bun run deploy:staging`
227
+ Deployment ID: dpl_qgthe6u4p7
228
+ ERR_LEASE_6011: lease expired, retrying with --fresh
229
+ </recalled-context>
230
+ ```
158
231
 
159
- Only 1% of finished work turns end in a reply shorter than 120 characters.
160
- Agents already close turns with something usable; eunoe just stops throwing it
161
- away.
232
+ Candidates come from BM25, the keyword ranking that search engines use,
233
+ which takes about 6 ms and no model. Optionally [Jev](https://typesafe.ai),
234
+ a small and very fast model, reorders the top 30 in about 300 ms, which
235
+ lifted the right excerpt from 88% to 95% of questions and beat every other
236
+ method and model that was tested. Without a key the BM25 order is used and
237
+ nothing else changes. The recall is computed once per message and pinned to
238
+ it, so it never disturbs the cache.
162
239
 
163
- In `trim` mode the system prompt tells the agent its final reply is the only
164
- thing it carries forward, and what belongs in it.
240
+ ## How it plugs in
165
241
 
166
- ## Interruptions and steering
242
+ ```
243
+ Claude Code ------> eunoe ------> api.anthropic.com
244
+ CLI, desktop app, 127.0.0.1:8788
245
+ Agent SDK rewrites /v1/messages
246
+ passes everything else through
247
+ ```
167
248
 
168
- Interrupted turns are where naive summarisation loses the most, so they get
169
- handled explicitly.
249
+ Claude Code honours `ANTHROPIC_BASE_URL` from its `settings.json`, so every
250
+ harness built on it works with no code changes, and because auth headers are
251
+ forwarded untouched your existing login keeps working whether it is a
252
+ subscription or an API key.
170
253
 
171
- | what happened | how eunoe treats it |
172
- |---|---|
173
- | **Esc mid-work**, then a new message | The interrupted turn ends there. Your new message, marker included, opens the next turn. |
174
- | **Typing while the agent works** | Claude Code sends a mid-conversation `system` message. It stays inside the running turn; in a reduced turn it is merged in right before the final reply. |
175
- | **An interrupted turn gets reduced** | No final reply exists, so eunoe writes one: `[This turn was interrupted before a final reply.] Last thing you said: … Tool calls (12): Edit src/app.ts; Bash \`bun test\`; …` |
176
- | **The cut lands on an interrupted turn** | Kept verbatim. The agent sees exactly where it stopped. |
177
- | **Esc-Esc rewind past the cut** | eunoe notices its cut turn is gone from the history and starts over from whatever Claude Code sends. |
254
+ Claude Code still keeps the whole conversation on disk at
255
+ `~/.claude/projects/<project>/<session>.jsonl`, since eunoe only changes
256
+ what goes out on the wire.
178
257
 
179
- ## Searching the transcript
258
+ ## The transcript
180
259
 
181
- Nothing is lost, so the agent is told where everything is and how to look.
182
- `search` picks the form:
260
+ Nothing is ever lost, because eunoe keeps a readable copy of the whole
261
+ session and teaches the agent to use it. The session file is rendered into a
262
+ folder of markdown under `~/.eunoe/transcripts/<session>/`, with one file
263
+ per turn and an index.
183
264
 
184
- | `search` | what the agent gets |
185
- |---|---|
186
- | `jsonl` | Claude Code's raw session file. Complete, but one long JSON-escaped line per entry. |
187
- | **`markdown`** (default) | `index.md` — one line per turn, what was asked and how it ended — plus `turn-0001.md`… with the message, every tool call tagged `[tool 3/12]` with its output, and the final reply. |
188
- | `xml` | The same, as `<turn>` / `<user>` / `<tool n name>` / `<output>` / `<final_reply>`. Easier for the model to parse, slightly harder to grep. |
189
- | `qmd` | The markdown folder indexed by [qmd](https://github.com/tobi/qmd) for semantic search, for when the agent doesn't know the exact words. |
265
+ ```
266
+ index.md one line per turn: what you asked and how the turn ended
267
+ turn-0001.md ## User your message
268
+ turn-0002.md ### [tool 3/12] every tool call, with its full output below it
269
+ ... ## Final reply the agent's reply
270
+ ```
190
271
 
191
- Rendering is fast and idempotent — a 199-turn session takes 0.13s — so it just
192
- runs again as the session grows. qmd's embedding pass takes ~83s and runs in the
193
- background; queries take 3–20s.
272
+ Markdown is far easier for an agent to grep and read than Claude Code's raw
273
+ JSONL, and the folder is rebuilt as the session grows. The agent is given the
274
+ path and a short recipe for searching it, so when it needs an exact detail
275
+ that neither evidence nor recall surfaced, it can find it in a couple of
276
+ commands instead of guessing. In the benchmark that happened about once
277
+ every ten sessions.
194
278
 
195
279
  ## Prompt caching
196
280
 
197
- A compaction scheme that breaks the cache costs more than it saves. Cache writes
198
- are 1.25× the input price and reads are 0.05×, so rewriting a cached prefix is
199
- 25× more expensive than reading it.
200
-
201
- eunoe's output is deterministic. The same history always produces the same bytes,
202
- a reduced turn always reduces identically, and nothing time-based is ever
203
- inserted. The prefix changes only at a cut.
281
+ Cache writes cost 1.25 times the input price and reads a tenth of it, so a
282
+ context scheme that breaks the cache costs more than it saves. eunoe's
283
+ output is deterministic, which means the same history always produces the
284
+ same bytes, recall is pinned to the message it was computed for and nothing
285
+ time-based is ever inserted, so the prefix only ever changes at a fold.
204
286
 
205
287
  ```
206
- turns 1–39 byte-identical to no proxy at all → cache hits as usual
207
- turn 40 CUT: one new prefix, written once → one cache write
208
- turns 41+ same prefix + new turns appended → reads hit again
288
+ turns 1-20 byte-identical to no proxy at all -> cache hits as usual
289
+ turn 21 fold: one new prefix, written once -> one cache write
290
+ turns 22-58 same prefix, new turns appended -> reads hit again
291
+ turn 59 next fold
209
292
  ```
210
293
 
211
- Measured through the proxy, right after a cut: **180k tokens read from cache,
212
- 1.5k written.** Claude Code's own compaction instead reads the entire ~900k
213
- context into a summary call and *then* writes a new prefix.
214
-
215
- ## Edge cases
294
+ Measured through the proxy right after a fold, 180k tokens came from the
295
+ cache and 1.5k were written. Claude Code's own compaction reads the entire
296
+ context into a summary call and then writes a new prefix anyway.
216
297
 
217
- The interesting ones are all about what happens when the cut itself doesn't fit.
298
+ ## Modes
218
299
 
219
- | case | behaviour |
300
+ | mode | |
220
301
  |---|---|
221
- | **Where a cut lands** | It aims for 30% of the room available — the window minus the fixed system prompt and tool definitions. Land at 70% and you compact again two turns later. |
222
- | **The cut doesn't fit** | Shrink in order, stopping as soon as it fits. **1.** Drop reduced turns, oldest first. **2.** Only then clear the *oldest* tool outputs of the live turn, half of what remains per step, shortening oversized inputs too. |
223
- | **Never removed from the live turn** | Your message, the agent's own text, every tool call, and the **latest 5 tool outputs** — what it is actually working from. |
224
- | **Compaction loops** | Impossible by construction. A cut only ever moves to a newer turn, every shrink step only removes more, and each step is persisted so the bytes stay cache-stable. |
225
- | **Nothing left to remove** | Sends anyway and logs `stuck`. |
226
- | **Context window** | Asked once per model via the Models API (`max_input_tokens`) and cached in `~/.eunoe/models.json`. If the lookup fails the window is unknown and **eunoe does not compact** — it won't cut against a limit it can't verify. |
227
- | **Images and PDFs** | Counted at ~1.6k tokens each, not at their base64 length. |
228
- | **Subagents, titles, side calls** | Each conversation is its own thread (session id + a fingerprint of its first turn) with its own cut. |
229
- | **Mid-conversation system messages** | Never dropped. The API only accepts them directly before an assistant message, so they are merged into that position. |
230
- | **Thinking blocks** | Kept with their signatures in the live turn, where tool use requires them. Dropped from reduced turns. |
231
- | **eunoe is down** | Every Claude request fails. launchd keeps it alive; `uninstall` leaves it running as a passthrough so sessions already pointed at it survive, and `stop` removes it once they're restarted. |
232
-
233
- ## Early results
234
-
235
- A full, pre-registered benchmark is in progress and will be published with its
236
- harness, raw data and every per-point result. Until then, here is what an
237
- earlier internal run showed, with its limits stated.
238
-
239
- **Setup.** 45 cut points across 14 projects, from one developer's real Claude
240
- Code history: real auto-compactions, plus long sessions cut where they crossed
241
- 350k tokens. At each point, every strategy answered the same 10 questions about
242
- the session — recent details, what was worked on 5–15 turns earlier, standing
243
- instructions, decisions, and what was in progress — through a real Claude Code
244
- session, with read-only access to a transcript that stopped at the cut. Answers
245
- were judged blind.
246
-
247
- | strategy | score | context after the cut | vs native, 95% CI |
248
- |---|---|---|---|
249
- | Claude Code's compaction | 84% | 82k | — |
250
- | **eunoe** | **95%** | 183k | **+11.0%** [+8.1, +13.9] |
251
- | eunoe, `trim` mode | 95% | 179k | +11.6% [+8.8, +14.4] |
252
- | eunoe, last 5 turns only | 90% | 164k | +6.4% [+2.4, +10.1] |
253
- | no compaction at all (ceiling) | 97% | 348k | — (7 points) |
254
-
255
- eunoe came within two points of never compacting at all, at about half the
256
- context. Claude Code's compaction lost most on what was worked on mid-session
257
- (74%), decisions (77%) and what was in progress (77%).
258
-
259
- **What this does not show yet.** The run had no budget-matched baseline — a plain
260
- sliding window given the same token budget — so it can't separate "eunoe keeps
261
- the right things" from "eunoe keeps more". It also used one judge, and many of its
262
- questions were easy enough that every strategy passed them. The full benchmark
263
- adds that baseline, a second judge, harder questions vetted before scoring, and a
264
- task-continuation test, and reports whatever it finds.
302
+ | **`default`** | everything above, recommended |
303
+ | **`compact`** | only the compaction is replaced: the context grows as it normally would, and at the limit eunoe folds instead of summarising, so you get the recall but none of the savings along the way |
304
+ | **`off`** | passthrough, Claude Code compacts on its own |
305
+
306
+ ```sh
307
+ eunoe mode # pick one
308
+ eunoe mode compact # or name it
309
+ ```
310
+
311
+ Whichever mode is on owns compaction, so eunoe disables Claude Code's
312
+ auto-compact while it is active and hands it back on `off` or `stop`.
265
313
 
266
314
  ## Usage
267
315
 
268
316
  ```sh
269
- eunoe serve # run in the foreground
270
- eunoe mode compact # off | trim | compact
271
- eunoe status # config, plus cache stats for recent requests
272
- eunoe install # background proxy + Claude Code settings
273
- eunoe uninstall # new sessions bypass eunoe; proxy stays up as a passthrough
274
- eunoe stop # remove the background proxy
317
+ eunoe start # route Claude Code through eunoe (sets up the proxy the first time)
318
+ eunoe stop # new sessions bypass eunoe; running ones keep working
319
+ eunoe mode # pick default | compact | off
320
+ eunoe settings # pick a setting and change it
321
+ eunoe settings list # show them all
322
+ eunoe settings set keep 5 # change one directly
323
+ eunoe status # what's running, every session, context and tokens saved
324
+ eunoe uninstall # stop and remove the background proxy
275
325
  ```
276
326
 
277
- `~/.eunoe/config.json`:
278
-
279
- ```json
280
- {
281
- "mode": "compact",
282
- "compactAt": 0.9,
283
- "keepTurns": "all",
284
- "search": "markdown",
285
- "port": 8788,
286
- "claudeConfigDirs": []
287
- }
288
- ```
327
+ `ee` is an alias, so `ee status` and `eunoe status` are the same command.
328
+
329
+ ### Per session
289
330
 
290
- Tools that run Claude Code with their own config directory (one per account,
291
- say) need eunoe to know about it, both to find their transcripts and to route
292
- them through the proxy:
331
+ Everything above takes `--session <id>` to apply to one session only. Session
332
+ ids are in Claude Code's `/status`, `eunoe status` lists the ones it has seen
333
+ with their working directory, and a unique prefix is enough.
293
334
 
294
335
  ```sh
295
- eunoe install --config-dir ~/.my-tool/claude-account-1
336
+ eunoe start --session 1baada86 # manage only this session, pass the rest through untouched
337
+ eunoe mode compact --session 1baada86
338
+ eunoe settings set keep 5 --session 1baada86
339
+ eunoe settings set keep default --session 1baada86 # back to the shared value
340
+ eunoe stop --session 1baada86
296
341
  ```
297
342
 
298
- The directory is remembered in `claudeConfigDirs`.
343
+ Sessions that aren't managed still go through the proxy, so any of them can
344
+ be switched on later without a restart, and every change applies from the
345
+ session's next request.
299
346
 
300
- `compactAt` is a share of the model's real context window. Claude Code compacts
301
- around 93%, so eunoe goes slightly earlier at 90% to win the race.
347
+ ### Settings
302
348
 
303
- Every request is logged to `~/.eunoe/requests.jsonl` with what eunoe did to it
304
- and the cache read/write that came back.
349
+ | key | default | what it does |
350
+ |---|---|---|
351
+ | `keep` | `3` | turns kept whole at the end |
352
+ | `budget` | `100000` | tokens of older detail that trigger a fold |
353
+ | `compactAt` | `0.9` | share of the context window that triggers the full cut |
354
+ | `keepTurns` | `all` | folded turns kept after a full cut |
355
+ | `search` | `markdown` | transcript form for the agent: `markdown`, `xml`, `jsonl` or `qmd` (semantic, via [qmd](https://github.com/tobi/qmd)) |
356
+ | `rerank` | `jev-preview` | Typesafe model that reorders recall, or `off` |
357
+
358
+ `~/.eunoe/config.json` holds the same keys plus `port`, `claudeConfigDirs`
359
+ and `sessions`. Tools that run Claude Code from their own config directory
360
+ need eunoe to know about it, which `eunoe start --config-dir <dir>` does
361
+ once and remembers.
362
+
363
+ ### Better recall with Jev
364
+
365
+ Recall works out of the box, but it gets noticeably better with
366
+ [Jev](https://typesafe.ai) reordering the candidates. To turn that on, put a
367
+ Typesafe API key in `~/.eunoe/typesafe-key` or in the `TYPESAFE_API_KEY`
368
+ environment variable, and `eunoe status` will show the reranker as active.
369
+ It can't slow anything down, because if Jev errors or takes longer than four
370
+ seconds the recall simply uses the plain keyword order instead. To switch it
371
+ off, run `eunoe settings set rerank off`.
372
+
373
+ ### Logs
374
+
375
+ Every request is written to `~/.eunoe/requests.jsonl` with what eunoe did
376
+ to it and how much of it the API served from cache, which is also where
377
+ `eunoe status` gets its numbers.
305
378
 
306
379
  ## What it doesn't do
307
380
 
308
- - **It is a proxy, so it is a single point of failure.** If it isn't running, and
309
- `ANTHROPIC_BASE_URL` still points at it, every Claude request fails.
310
- - **It only understands Claude Code's shape** of conversation — first-message
311
- system reminders, mid-conversation system messages, interruption markers. Other
312
- Anthropic API clients pass through unchanged but get nothing out of it.
313
- - **It can't recover what Claude Code already compacted.** Turn it on before a
314
- long session, not after.
315
- - **`trim` mode changes how the agent should write.** It's told to put state in
316
- its final reply, and it mostly does — but a sloppy final reply costs more in
317
- `trim` than in `compact`.
318
-
319
- ## Prior art
320
-
321
- Claude Code's own compaction is the thing being replaced, and the summary format
322
- it produces is a reasonable piece of prompt engineering — it just runs after the
323
- conversation is already gone. [qmd](https://github.com/tobi/qmd) does the local
324
- semantic search for the `qmd` mode.
381
+ - **It is a proxy, which makes it a single point of failure.** If it isn't
382
+ running while `ANTHROPIC_BASE_URL` still points at it, every Claude request
383
+ fails. launchd keeps it alive, and `stop` leaves it running so sessions
384
+ already pointed at it keep working.
385
+ - **It only understands Claude Code's shape of conversation.** Other
386
+ Anthropic API clients pass through unchanged and get nothing out of it.
387
+ - **It can't recover what Claude Code already compacted,** so turn it on
388
+ before a long session rather than after.
389
+ - **Recall is keyword search.** Ask about "the deploy that failed" and it
390
+ finds the deploy output, but ask about "that thing from before" and it
391
+ finds nothing, leaving the agent to search the transcript itself. Jev
392
+ narrows this gap without closing it.
393
+ - **It never cuts against a limit it can't verify.** The context window size
394
+ comes from the Models API for each model, and if that lookup fails the
395
+ budget folds still happen but the full cut does not.
396
+
397
+ ## Credits
398
+
399
+ - [qmd](https://github.com/tobi/qmd) by Tobi Lütke provides the local
400
+ semantic search behind the `qmd` setting.
401
+ - [Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev) by
402
+ Typesafe is the system-one model that reranks recall.
325
403
 
326
404
  ## License
327
405
 
328
- MIT
406
+ eunoe is licensed under the [MIT License](LICENSE), and welcomes
407
+ contributions and suggestions.