@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.
- package/README.md +364 -239
- package/package.json +20 -3
- package/src/cli/{types.ts → command.ts} +1 -4
- package/src/cli/commands/index.ts +27 -3
- package/src/cli/commands/load.ts +44 -0
- package/src/cli/commands/mode.ts +68 -24
- package/src/cli/commands/peek.ts +22 -0
- package/src/cli/commands/replay.ts +194 -0
- package/src/cli/commands/serve.ts +24 -8
- package/src/cli/commands/sessions.ts +70 -0
- package/src/cli/commands/settings.ts +119 -0
- package/src/cli/commands/start.ts +73 -0
- package/src/cli/commands/status.ts +107 -30
- package/src/cli/commands/stop.ts +36 -12
- package/src/cli/commands/uninstall.ts +14 -14
- package/src/cli/commands/watch.ts +16 -0
- package/src/cli/logo/art.ts +99 -0
- package/src/cli/logo/index.ts +12 -18
- package/src/cli/main.ts +3 -3
- package/src/cli/proxy/claude-settings.ts +59 -0
- package/src/cli/proxy/inspect.ts +67 -0
- package/src/cli/proxy/launchd.ts +53 -0
- package/src/cli/proxy/service.ts +53 -0
- package/src/cli/proxy/stats.ts +81 -0
- package/src/cli/proxy/watch.ts +60 -0
- package/src/cli/replay/index.ts +141 -0
- package/src/cli/replay/worker.ts +15 -0
- package/src/cli/sessions/browse.ts +88 -0
- package/src/cli/sessions/index.ts +76 -0
- package/src/cli/sessions/source.ts +74 -0
- package/src/cli/{args.ts → terminal/args.ts} +1 -1
- package/src/cli/{help.ts → terminal/help.ts} +25 -9
- package/src/cli/terminal/prompt.ts +114 -0
- package/src/cli/terminal/ui.ts +137 -0
- package/src/config/index.ts +128 -14
- package/src/config/{constants.ts → paths.ts} +4 -14
- package/src/context/content.ts +51 -3
- package/src/context/fold/evidence.ts +145 -0
- package/src/context/fold/index.ts +65 -0
- package/src/context/fold/strategies.ts +94 -0
- package/src/context/fold/turns.ts +76 -0
- package/src/context/prompts.ts +141 -22
- package/src/context/recall/index.ts +214 -0
- package/src/context/recall/rerank.ts +71 -0
- package/src/proxy/canonical.ts +40 -0
- package/src/proxy/compaction.ts +122 -52
- package/src/proxy/estimate.ts +14 -9
- package/src/proxy/fingerprint.ts +29 -0
- package/src/proxy/handoff.ts +71 -0
- package/src/proxy/log.ts +2 -2
- package/src/proxy/recall.ts +58 -0
- package/src/proxy/rewrite.ts +152 -56
- package/src/proxy/server.ts +122 -47
- package/src/proxy/sessions.ts +64 -0
- package/src/proxy/state.ts +17 -3
- package/src/proxy/usage.ts +50 -34
- package/src/proxy/window.ts +32 -34
- package/src/transcript/format.ts +88 -25
- package/src/transcript/locate.ts +38 -17
- package/src/transcript/messages.ts +18 -0
- package/src/transcript/parse.ts +137 -56
- package/src/transcript/render.ts +44 -17
- package/src/transcript/search.ts +20 -56
- package/src/transcript/subagents.ts +150 -0
- package/src/cli/claude-settings.ts +0 -42
- package/src/cli/commands/install.ts +0 -44
- package/src/cli/constants.ts +0 -29
- package/src/cli/inspect.ts +0 -41
- package/src/cli/launchd.ts +0 -34
- package/src/cli/logo/pixels.ts +0 -34
- package/src/cli/ui.ts +0 -91
- package/src/config/types.ts +0 -21
- package/src/context/constants.ts +0 -7
- package/src/context/strategies.ts +0 -89
- package/src/context/turns.ts +0 -82
- package/src/context/types.ts +0 -24
- package/src/proxy/constants.ts +0 -19
- package/src/proxy/types.ts +0 -58
- package/src/transcript/constants.ts +0 -16
- 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
|
|
10
|
-
no
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
|
53
|
-
|
|
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). `
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
##
|
|
112
|
+
## How it works
|
|
60
113
|
|
|
61
|
-
|
|
62
|
-
|
|
114
|
+
eunoe does three things, and each one was measured on its own before it was
|
|
115
|
+
kept.
|
|
63
116
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
144
|
+
The same turn after eunoe folds it (about 700 tokens):
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
[you] fix the failing lease test
|
|
96
148
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
149
|
+
[evidence] Bash bun test
|
|
150
|
+
FAIL lease renews after expiry
|
|
151
|
+
expected 6011, got undefined
|
|
152
|
+
3 pass, 1 fail
|
|
100
153
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
```
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
221
|
+
### Recall
|
|
144
222
|
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
228
|
+
```
|
|
229
|
+
you (turn 71) deploy is failing with ERR_LEASE_6011 again
|
|
150
230
|
|
|
151
|
-
|
|
231
|
+
eunoe appends:
|
|
152
232
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
Agents already close turns with something usable; eunoe just stops throwing it
|
|
161
|
-
away.
|
|
248
|
+
## How it plugs in
|
|
162
249
|
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
207
|
-
turn
|
|
208
|
-
turns
|
|
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
|
|
212
|
-
1.5k written
|
|
213
|
-
context into a summary call and
|
|
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
|
-
|
|
313
|
+
## Modes
|
|
218
314
|
|
|
219
|
-
|
|
|
315
|
+
| mode | |
|
|
220
316
|
|---|---|
|
|
221
|
-
|
|
|
222
|
-
|
|
|
223
|
-
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
270
|
-
eunoe
|
|
271
|
-
eunoe
|
|
272
|
-
eunoe
|
|
273
|
-
eunoe
|
|
274
|
-
eunoe
|
|
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
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
361
|
+
eunoe sessions [words] # browse earlier sessions here; enter copies {ee:continue:<id>}
|
|
296
362
|
```
|
|
297
363
|
|
|
298
|
-
The
|
|
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
|
-
|
|
301
|
-
around 93%, so eunoe goes slightly earlier at 90% to win the race.
|
|
368
|
+
### Per session
|
|
302
369
|
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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.
|