@chroxify/eunoe 0.1.0 → 1.1.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 (80) hide show
  1. package/README.md +364 -239
  2. package/package.json +20 -3
  3. package/src/cli/{types.ts → command.ts} +1 -4
  4. package/src/cli/commands/index.ts +27 -3
  5. package/src/cli/commands/load.ts +44 -0
  6. package/src/cli/commands/mode.ts +68 -24
  7. package/src/cli/commands/peek.ts +22 -0
  8. package/src/cli/commands/replay.ts +194 -0
  9. package/src/cli/commands/serve.ts +24 -8
  10. package/src/cli/commands/sessions.ts +70 -0
  11. package/src/cli/commands/settings.ts +119 -0
  12. package/src/cli/commands/start.ts +73 -0
  13. package/src/cli/commands/status.ts +107 -30
  14. package/src/cli/commands/stop.ts +36 -12
  15. package/src/cli/commands/uninstall.ts +14 -14
  16. package/src/cli/commands/watch.ts +16 -0
  17. package/src/cli/logo/art.ts +99 -0
  18. package/src/cli/logo/index.ts +12 -18
  19. package/src/cli/main.ts +3 -3
  20. package/src/cli/proxy/claude-settings.ts +59 -0
  21. package/src/cli/proxy/inspect.ts +67 -0
  22. package/src/cli/proxy/launchd.ts +53 -0
  23. package/src/cli/proxy/service.ts +53 -0
  24. package/src/cli/proxy/stats.ts +81 -0
  25. package/src/cli/proxy/watch.ts +60 -0
  26. package/src/cli/replay/index.ts +141 -0
  27. package/src/cli/replay/worker.ts +15 -0
  28. package/src/cli/sessions/browse.ts +88 -0
  29. package/src/cli/sessions/index.ts +76 -0
  30. package/src/cli/sessions/source.ts +74 -0
  31. package/src/cli/{args.ts → terminal/args.ts} +1 -1
  32. package/src/cli/{help.ts → terminal/help.ts} +25 -9
  33. package/src/cli/terminal/prompt.ts +114 -0
  34. package/src/cli/terminal/ui.ts +137 -0
  35. package/src/config/index.ts +128 -14
  36. package/src/config/{constants.ts → paths.ts} +4 -14
  37. package/src/context/content.ts +51 -3
  38. package/src/context/fold/evidence.ts +145 -0
  39. package/src/context/fold/index.ts +65 -0
  40. package/src/context/fold/strategies.ts +94 -0
  41. package/src/context/fold/turns.ts +76 -0
  42. package/src/context/prompts.ts +141 -22
  43. package/src/context/recall/index.ts +214 -0
  44. package/src/context/recall/rerank.ts +71 -0
  45. package/src/proxy/canonical.ts +40 -0
  46. package/src/proxy/compaction.ts +122 -52
  47. package/src/proxy/estimate.ts +14 -9
  48. package/src/proxy/fingerprint.ts +29 -0
  49. package/src/proxy/handoff.ts +71 -0
  50. package/src/proxy/log.ts +2 -2
  51. package/src/proxy/recall.ts +58 -0
  52. package/src/proxy/rewrite.ts +152 -56
  53. package/src/proxy/server.ts +122 -47
  54. package/src/proxy/sessions.ts +64 -0
  55. package/src/proxy/state.ts +17 -3
  56. package/src/proxy/usage.ts +50 -34
  57. package/src/proxy/window.ts +32 -34
  58. package/src/transcript/format.ts +88 -25
  59. package/src/transcript/locate.ts +38 -17
  60. package/src/transcript/messages.ts +18 -0
  61. package/src/transcript/parse.ts +137 -56
  62. package/src/transcript/render.ts +44 -17
  63. package/src/transcript/search.ts +20 -56
  64. package/src/transcript/subagents.ts +150 -0
  65. package/src/cli/claude-settings.ts +0 -42
  66. package/src/cli/commands/install.ts +0 -44
  67. package/src/cli/constants.ts +0 -29
  68. package/src/cli/inspect.ts +0 -41
  69. package/src/cli/launchd.ts +0 -34
  70. package/src/cli/logo/pixels.ts +0 -34
  71. package/src/cli/ui.ts +0 -91
  72. package/src/config/types.ts +0 -21
  73. package/src/context/constants.ts +0 -7
  74. package/src/context/strategies.ts +0 -89
  75. package/src/context/turns.ts +0 -82
  76. package/src/context/types.ts +0 -24
  77. package/src/proxy/constants.ts +0 -19
  78. package/src/proxy/types.ts +0 -58
  79. package/src/transcript/constants.ts +0 -16
  80. package/src/transcript/types.ts +0 -43
package/README.md CHANGED
@@ -6,323 +6,448 @@
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
 
14
- > At the top of Dante's Purgatory two rivers run from one spring. **Lethe**
15
- > washes away the memory of what weighed on you. **Eunoe** — *good mind* —
16
- > gives back the memory of the good. You drink from both. Lethe first.
17
-
18
14
  ---
19
15
 
20
16
  ## The problem
21
17
 
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.
25
-
26
- You can feel it the moment it happens.
27
-
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
- ```
18
+ Every long Claude Code session eventually turns dumb on you, and it happens
19
+ well before the context window is full. The agent starts forgetting rules you
20
+ gave it an hour ago, re-reading files it already read, and asking about
21
+ decisions you made together. When compaction finally kicks in it makes things
22
+ worse, because the whole conversation gets replaced by a summary and anything
23
+ the summary left out is gone for good.
34
24
 
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.
25
+ At that point the only fix is a fresh session, which means explaining the
26
+ project again, pasting the rules again, and hoping it gets further this time.
38
27
 
39
- Compaction keeps the gist. The gist is not what you need.
28
+ The reason is that every file the agent ever opened and every command it ever
29
+ ran stays in the prompt, so two hundred turns in the model is still reading a
30
+ log from the first hour on every request, with the thing you said five turns
31
+ ago buried somewhere in the middle of it.
40
32
 
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.
33
+ | | Claude Code | eunoe |
34
+ |---|---|---|
35
+ | **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 |
36
+ | **at the limit** | a two-minute summary call replaces the whole conversation | never reached |
37
+ | **old details** | gone unless the summary kept them | come back the moment a message is about them |
38
+ | **your instructions** | survive if the summary mentions them | kept word for word |
39
+ | **the session** | one window, then you start over | as sharp at turn three hundred as at turn three |
40
+
41
+ ## Results
42
+
43
+ Measured on 24 held-out sessions of 90 to 120 turns with 600 facts planted
44
+ at known depths, such as a value a command printed once, an error, an
45
+ instruction that was later revised or a decision with its rejected options,
46
+ and scored by exact match with no judge involved.
47
+
48
+ > These numbers come from one developer's sessions. They are a careful
49
+ > measurement, not a general one, so read them as a strong signal rather than
50
+ > a guarantee for your own work.
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).
88
+
89
+ Context management is hard to benchmark well alone, because the honest test
90
+ is many people's real sessions. If you run the bench on your own sessions,
91
+ find a case where eunoe does worse, or have a better way to measure this,
92
+ please open an issue or a pull request. Results from other setups are the
93
+ contribution I'd value most.
44
94
 
45
95
  ## Install
46
96
 
47
97
  ```sh
48
- bun add -g @chroxify/eunoe # puts `eunoe` on your PATH
49
- eunoe install # background proxy + ANTHROPIC_BASE_URL in ~/.claude/settings.json
98
+ bun add -g @chroxify/eunoe # puts `eunoe` and the short `ee` on your PATH
99
+ eunoe start # sets up the background proxy, routes Claude Code through it
50
100
  ```
51
101
 
52
- Then use Claude Code exactly as before. New sessions go through eunoe; restart
53
- any that are already running. `eunoe uninstall` reverses it.
102
+ Then use Claude Code exactly as before. New sessions go through eunoe, and
103
+ sessions that were already running pick it up when you restart them.
104
+ `eunoe stop` turns it off again.
54
105
 
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.
106
+ Requires [Bun](https://bun.sh). On macOS `start` sets up a launchd agent that
107
+ keeps the proxy alive and a watchdog that unroutes Claude Code if the proxy
108
+ should ever be down; on other platforms keep `eunoe serve` running however you like and
109
+ everything else works the same. The proxy listens on 127.0.0.1:38663 (`port`
110
+ in `~/.eunoe/config.json` changes it).
58
111
 
59
- ## What it does instead
112
+ ## How it works
60
113
 
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.
114
+ eunoe does three things, and each one was measured on its own before it was
115
+ kept.
63
116
 
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.
117
+ ### Folding
118
+
119
+ A **turn** is one message from you and everything the agent did until your
120
+ next one. Once a turn is finished, most of what it holds is tool output the
121
+ agent needed in the moment and will never read again.
122
+
123
+ A finished turn, as Claude Code keeps it (about 9,000 tokens):
67
124
 
68
125
  ```
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
126
+ [you] fix the failing lease test
127
+
128
+ [agent] I'll look at the test first.
129
+ [tool] Read src/lease.test.ts
130
+ ... 140 lines of file ...
131
+ [tool] Bash bun test
132
+ ... 80 lines of output ...
133
+
134
+ [agent] The expiry check is off by one.
135
+ [tool] Edit src/lease.ts
136
+ [tool] Bash bun test
137
+ ... 80 lines of output ...
138
+
139
+ [agent] Fixed the off-by-one in renew(): the expiry check used <= instead
140
+ of <. All 4 tests pass. The 6011 code comes from LEASE_EXPIRED in
141
+ src/errors.ts.
90
142
  ```
91
143
 
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.
144
+ The same turn after eunoe folds it (about 700 tokens):
145
+
146
+ ```
147
+ [you] fix the failing lease test
96
148
 
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.
149
+ [evidence] Bash bun test
150
+ FAIL lease renews after expiry
151
+ expected 6011, got undefined
152
+ 3 pass, 1 fail
100
153
 
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.
154
+ [agent] Fixed the off-by-one in renew(): the expiry check used <= instead
155
+ of <. All 4 tests pass. The 6011 code comes from LEASE_EXPIRED in
156
+ src/errors.ts.
157
+ ```
105
158
 
106
- ## How it plugs in
159
+ The agent already writes a usable summary of every turn in its final reply,
160
+ so folding keeps that reply, your message and the evidence described below,
161
+ and drops everything else. A hundred folded turns come to about 70k tokens,
162
+ which is why eunoe never has to throw a turn away.
163
+
164
+ The timing is what makes this cheap. The last 3 turns are always sent whole,
165
+ down to the last tool call, and older turns stay whole too until the detail
166
+ in them passes a budget of 100k tokens. At that point every turn except
167
+ those 3 folds in one step.
107
168
 
108
169
  ```
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
170
+ 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
171
+ -----------------------------------------------------------------------
172
+ t=10 W W W W W W W W W L budget not reached
173
+ t=20 W W W W W W W W W W W W W W W W W W W L
174
+ t=21 F F F F F F F F F F F F F F F F F F W W L <- fold
175
+ 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
176
+
177
+ W whole F folded L the live turn
113
178
  ```
114
179
 
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.
118
-
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.
180
+ It happens in a step rather than a slide because of the prompt cache. The
181
+ cache is a prefix match, so changing anything in the middle rewrites
182
+ everything after it at 1.25 times the price of a read. Folding one turn per
183
+ message would rewrite the cache on every message, whereas folding a budget
184
+ at a time writes the new prefix once and lets every request until the next
185
+ step read it at a tenth of the price.
122
186
 
123
- ## Modes
187
+ The first message is always kept verbatim, because Claude Code packs
188
+ `CLAUDE.md`, your git status and your skills into it and its hash travels in
189
+ a billing header. The live turns are untouched too, thinking blocks and
190
+ interruptions included. If a turn was interrupted before the agent replied,
191
+ the fold keeps its last words and the list of what it ran instead.
124
192
 
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 |
193
+ ### Evidence
130
194
 
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.
195
+ Folding on its own would lose the exact values in tool output, like the
196
+ deployment id, the error code or the test count, so when a turn folds each
197
+ command's output is scored line by line and the lines worth keeping are
198
+ quoted verbatim under the command that produced them.
135
199
 
136
- ```sh
137
- eunoe mode compact # or trim, or off
200
+ ```
201
+ score line
202
+ ----- ------------------------------------------------------------
203
+ +3 FAIL lease renews after expiry error words and codes
204
+ +3 Deployment ID: dpl_qgthe6u4p7 ids, hashes, URLs
205
+ +2 3 pass, 1 fail test and diff summaries
206
+ +2 ERR_LEASE_6011: lease expired error codes
207
+ +2 took 412 ms durations, sizes, versions
208
+ +1 status: applied key: value
209
+ 0 Compiling 14 modules... dropped
210
+ 0 > lease@1.4.0 test dropped
138
211
  ```
139
212
 
140
- Whichever is on owns compaction: eunoe disables Claude Code's auto-compact while
141
- active and hands it back on `off` or `uninstall`.
213
+ Every command keeps its best lines, and the highest scorers across the turn
214
+ fill a budget of about 4,000 characters, which is a few hundred tokens. The
215
+ cap exists because a long test or build log would otherwise fold to
216
+ something bigger than the turn it came from, and the size was chosen on the
217
+ development split, where it kept 97% of the planted values. Commands whose
218
+ output can simply be run again, such as `Read`, `Grep`, `Glob`, `Edit` and
219
+ `Write`, are skipped entirely.
142
220
 
143
- ## What counts as a final reply
221
+ ### Recall
144
222
 
145
- A **turn** starts with a message you sent — text or an image, not a tool result —
146
- and runs until your next one.
223
+ Each time you send a message, before the agent responds, eunoe uses it as a
224
+ search query over every tool output that is no longer in context and
225
+ everything the agent said while working. Matching excerpts are quoted
226
+ exactly and appended to your message, oldest first.
147
227
 
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.
228
+ ```
229
+ you (turn 71) deploy is failing with ERR_LEASE_6011 again
150
230
 
151
- Across 18,966 real turns:
231
+ eunoe appends:
152
232
 
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 |
233
+ <recalled-context>
234
+ [turn 12] Bash `bun run deploy:staging`
235
+ Deployment ID: dpl_qgthe6u4p7
236
+ ERR_LEASE_6011: lease expired, retrying with --fresh
237
+ </recalled-context>
238
+ ```
239
+
240
+ Candidates come from BM25, the keyword ranking that search engines use,
241
+ which takes about 6 ms and no model. Optionally [Jev](https://typesafe.ai),
242
+ a small and very fast model, reorders the top 30 in about 300 ms, which
243
+ lifted the right excerpt from 88% to 95% of questions and beat every other
244
+ method and model that was tested. Without a key the BM25 order is used and
245
+ nothing else changes. The recall is computed once per message and pinned to
246
+ it, so it never disturbs the cache.
158
247
 
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.
248
+ ## How it plugs in
162
249
 
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.
250
+ ```
251
+ Claude Code ------> eunoe ------> api.anthropic.com
252
+ CLI, desktop app, 127.0.0.1:38663
253
+ Agent SDK rewrites /v1/messages
254
+ passes everything else through
255
+ ```
165
256
 
166
- ## Interruptions and steering
257
+ Claude Code honours `ANTHROPIC_BASE_URL` from its `settings.json`, so every
258
+ harness built on it works with no code changes, and because auth headers are
259
+ forwarded untouched your existing login keeps working whether it is a
260
+ subscription or an API key.
167
261
 
168
- Interrupted turns are where naive summarisation loses the most, so they get
169
- handled explicitly.
262
+ Claude Code still keeps the whole conversation on disk at
263
+ `~/.claude/projects/<project>/<session>.jsonl`, since eunoe only changes
264
+ what goes out on the wire.
170
265
 
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. |
266
+ ## The transcript
178
267
 
179
- ## Searching the transcript
268
+ Nothing is ever lost, because eunoe keeps a readable copy of the whole
269
+ session and teaches the agent to use it. The session file is rendered into a
270
+ folder of markdown under `~/.eunoe/transcripts/<session>/`, with one file
271
+ per turn and an index.
180
272
 
181
- Nothing is lost, so the agent is told where everything is and how to look.
182
- `search` picks the form:
273
+ ```
274
+ index.md one line per turn: what you asked and how the turn ended
275
+ turn-0001.md ## User your message
276
+ turn-0002.md ### [tool 3/12] every tool call, with its full output below it
277
+ ... ## Final reply the agent's reply
278
+ turn-0002-subagent-1.md the whole run of a subagent launched in that turn
279
+ ```
183
280
 
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. |
281
+ A subagent's run is its own file, indexed under the turn that launched it
282
+ and linked from the Agent call; the turn itself only has the subagent's
283
+ report. What the subagent saw and ran is also part of recall, labelled
284
+ `subagent "…"`, so the agent gets a value only its subagent printed without
285
+ having to look.
190
286
 
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.
287
+ Markdown is far easier for an agent to grep and read than Claude Code's raw
288
+ JSONL, and the folder is rebuilt as the session grows. The agent is given the
289
+ path and a short recipe for searching it, so when it needs an exact detail
290
+ that neither evidence nor recall surfaced, it can find it in a couple of
291
+ commands instead of guessing. In the benchmark that happened about once
292
+ every ten sessions.
194
293
 
195
294
  ## Prompt caching
196
295
 
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.
296
+ Cache writes cost 1.25 times the input price and reads a tenth of it, so a
297
+ context scheme that breaks the cache costs more than it saves. eunoe's
298
+ output is deterministic, which means the same history always produces the
299
+ same bytes, recall is pinned to the message it was computed for and nothing
300
+ time-based is ever inserted, so the prefix only ever changes at a fold.
204
301
 
205
302
  ```
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
303
+ turns 1-20 byte-identical to no proxy at all -> cache hits as usual
304
+ turn 21 fold: one new prefix, written once -> one cache write
305
+ turns 22-58 same prefix, new turns appended -> reads hit again
306
+ turn 59 next fold
209
307
  ```
210
308
 
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
309
+ Measured through the proxy right after a fold, 180k tokens came from the
310
+ cache and 1.5k were written. Claude Code's own compaction reads the entire
311
+ context into a summary call and then writes a new prefix anyway.
216
312
 
217
- The interesting ones are all about what happens when the cut itself doesn't fit.
313
+ ## Modes
218
314
 
219
- | case | behaviour |
315
+ | mode | |
220
316
  |---|---|
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.
317
+ | **`default`** | everything above, recommended |
318
+ | **`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 |
319
+ | **`off`** | passthrough, Claude Code compacts on its own |
320
+
321
+ ```sh
322
+ eunoe mode # pick one
323
+ eunoe mode compact # or name it
324
+ ```
325
+
326
+ Whichever mode is on owns compaction, so eunoe disables Claude Code's
327
+ auto-compact while it is active and hands it back on `off` or `stop`.
265
328
 
266
329
  ## Usage
267
330
 
268
331
  ```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
332
+ eunoe start # route Claude Code through eunoe (sets up the proxy the first time)
333
+ eunoe stop # new sessions bypass eunoe; running ones keep working
334
+ eunoe mode # pick default | compact | off
335
+ eunoe settings # pick a setting and change it
336
+ eunoe settings list # show them all
337
+ eunoe settings set keep 5 # change one directly
338
+ eunoe status # what's running, every session, context and tokens saved
339
+ eunoe replay # replay your whole Claude Code history through eunoe, offline, and see what it saves
340
+ eunoe uninstall # stop and remove the background proxy
275
341
  ```
276
342
 
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
- ```
343
+ `ee` is an alias, so `ee status` and `eunoe status` are the same command.
289
344
 
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:
345
+ ### Earlier sessions
346
+
347
+ A different big problem with Claude is that new chats always start cold. eunoe
348
+ helps you carry an earlier session into one: run `eunoe sessions`, pick the
349
+ session (type to filter), and paste the copied `{ee:continue:<id>}` marker into
350
+ the chat along with what you want done. eunoe attaches that whole session to
351
+ the message, folded the same way it folds turns in a live session: your
352
+ messages, the key output lines and the agent's replies, in order. From then on
353
+ the earlier session's dropped tool output is searchable too, so an exact value
354
+ from it comes back as recalled context when a later message is about it.
355
+
356
+ You always name the session. eunoe never guesses which one you mean or injects
357
+ one on its own; in the benchmark that choice was wrong a third of the time,
358
+ while a named session remembered 98% of its facts and kept 95% of its rules.
293
359
 
294
360
  ```sh
295
- eunoe install --config-dir ~/.my-tool/claude-account-1
361
+ eunoe sessions [words] # browse earlier sessions here; enter copies {ee:continue:<id>}
296
362
  ```
297
363
 
298
- The directory is remembered in `claudeConfigDirs`.
364
+ The agent is also taught two commands for digging on its own: `eunoe load
365
+ <id>` writes a session to `~/.eunoe/loaded/<id>.md` for it to read whole, and
366
+ `eunoe peek <id> <words>` prints just the excerpts that match.
299
367
 
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.
368
+ ### Per session
302
369
 
303
- Every request is logged to `~/.eunoe/requests.jsonl` with what eunoe did to it
304
- and the cache read/write that came back.
370
+ Everything above takes `--session <id>` to apply to one session only. Session
371
+ ids are in Claude Code's `/status`, `eunoe status` lists the ones it has seen
372
+ with their working directory, and a unique prefix is enough.
305
373
 
306
- ## What it doesn't do
374
+ ```sh
375
+ eunoe start --session 1baada86 # manage only this session, pass the rest through untouched
376
+ eunoe mode compact --session 1baada86
377
+ eunoe settings set keep 5 --session 1baada86
378
+ eunoe settings set keep default --session 1baada86 # back to the shared value
379
+ eunoe stop --session 1baada86
380
+ ```
381
+
382
+ Sessions that aren't managed still go through the proxy, so any of them can
383
+ be switched on later without a restart, and every change applies from the
384
+ session's next request.
307
385
 
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`.
386
+ ### Settings
318
387
 
319
- ## Prior art
388
+ | key | default | what it does |
389
+ |---|---|---|
390
+ | `keep` | `3` | turns kept whole at the end |
391
+ | `budget` | `100000` | tokens of older detail that trigger a fold |
392
+ | `turnCap` | `100000` | a kept turn larger than this is folded at once, the live turn excepted; `off` to disable |
393
+ | `compactAt` | `0.9` | share of the context window that triggers the full cut |
394
+ | `keepTurns` | `all` | folded turns kept after a full cut |
395
+ | `search` | `markdown` | transcript form for the agent: `markdown`, `xml`, `jsonl` or `qmd` (semantic, via [qmd](https://github.com/tobi/qmd)) |
396
+ | `rerank` | `jev-preview` | Typesafe model that reorders recall, or `off` |
397
+
398
+ `~/.eunoe/config.json` holds the same keys plus `port`, `claudeConfigDirs`
399
+ and `sessions`. Tools that run Claude Code from their own config directory
400
+ need eunoe to know about it, which `eunoe start --config-dir <dir>` does
401
+ once and remembers.
402
+
403
+ ### Better recall with Jev
404
+
405
+ Recall works out of the box, but it gets noticeably better with
406
+ [Jev](https://typesafe.ai) reordering the candidates. To turn that on, put a
407
+ Typesafe API key in `~/.eunoe/typesafe-key` or in the `TYPESAFE_API_KEY`
408
+ environment variable, and `eunoe status` will show the reranker as active.
409
+ It can't slow anything down, because if Jev errors or takes longer than four
410
+ seconds the recall simply uses the plain keyword order instead. To switch it
411
+ off, run `eunoe settings set rerank off`.
412
+
413
+ ### Logs
414
+
415
+ Every request is written to `~/.eunoe/requests.jsonl` with what eunoe did
416
+ to it and how much of it the API served from cache, which is also where
417
+ `eunoe status` gets its numbers.
418
+
419
+ ## What it doesn't do
320
420
 
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.
421
+ - **It is a proxy, which makes it a single point of failure.** If it isn't
422
+ running while `ANTHROPIC_BASE_URL` still points at it, every Claude request
423
+ fails. Three things limit the damage: `start` refuses to route Claude Code
424
+ until the proxy answers, launchd restarts the proxy within a second whenever
425
+ it dies, and a watchdog that runs every 10 seconds takes eunoe out of
426
+ `settings.json` after 20 seconds of silence and puts it back when the proxy
427
+ answers again. A request the proxy can't deliver upstream comes back as a
428
+ 502 that Claude Code retries, never a hang. Sessions that were already
429
+ running keep the old address until restarted, so they wait for the proxy
430
+ rather than falling back.
431
+ - **It only understands Claude Code's shape of conversation.** Other
432
+ Anthropic API clients pass through unchanged and get nothing out of it.
433
+ - **It can't recover what Claude Code already compacted,** so turn it on
434
+ before a long session rather than after.
435
+ - **Recall is keyword search.** Ask about "the deploy that failed" and it
436
+ finds the deploy output, but ask about "that thing from before" and it
437
+ finds nothing, leaving the agent to search the transcript itself. Jev
438
+ narrows this gap without closing it.
439
+ - **It never cuts against a limit it can't verify.** The context window size
440
+ comes from the Models API for each model, and if that lookup fails the
441
+ budget folds still happen but the full cut does not.
442
+
443
+ ## Credits
444
+
445
+ - [qmd](https://github.com/tobi/qmd) by Tobi Lütke provides the local
446
+ semantic search behind the `qmd` setting.
447
+ - [Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev) by
448
+ Typesafe is the system-one model that reranks recall.
325
449
 
326
450
  ## License
327
451
 
328
- MIT
452
+ eunoe is licensed under the [MIT License](LICENSE), and welcomes
453
+ contributions and suggestions.