Coding agent with a memory of all things
eukhe docs memory.md

Chat memory #

eukhe's chat memory is Victor Taelin's OptChat design ("UniiChat: one chat that never ends", gist 91837951, revision of 2026-10-08). The gist is the only source of truth. This page lists every rule of the gist, the Rust code that does it, and the law or the test that secures it.

  • The Rust code is in crates/eukhe-core/src/memory/ and crates/eukhe-core/src/durable/optchat/. Paths below start at crates/eukhe-core/src/.
  • The pure core (the view, the merge order, the sawtooth, the compaction view, the work queue) has a model in Bend: bend/optchat.bend.
  • LAWS.bend states what the model must do. PROOF.bend proves it.
  • The bridge test runs the Rust planner and the Bend model on the same scenarios. The two must print the same output, byte for byte.

Use #

The chat lives in ~/.eukhe/chat/. It is one append-only log. Each message has a kind: user, talk, tool, echo, or work (a subagent or background report). The compactor writes one-line summaries into a binary tree over the log. A summary has a 512-byte target; a longer one is tried again while it gets shorter, up to 5 times, and the shortest is cut to 512 bytes. A failed summary is tried again at the next message, and every 10 s while a turn waits for it; its third failure keeps its message cut to 512 bytes as the line. An idle chat makes no model call. A turn waits for the view's summaries at most memory.settleTimeoutMs.

The view is the context each root turn starts from. When the view passes 128 KB, one batch merges it down to 64 KB. The view is saved to view.json on each change, so the prompt cache stays valid across restarts. A cache mark sits on the last whole block of 4 lines. Compaction calls use their own view of 16 KB to 32 KB.

Command Effect
/memory Show the status and every view line
/memory zoom <id> <n> Show the messages or summaries of node <id>+<n>
/memory-model Select the compaction model and its reasoning effort (←/→ on a reasoning model; saved as memory.model and memory.thinking, default medium)

The status bar shows the view size as mem N/128K. Each compactor change shows as a memory: row. The summary text shows at full detail only.

Check it #

bend PROOF.bend             # ALL PROOFS CHECK
bend PROOF.bend --verdict   # the same, with the BendTT kernel (needs Lean 4.34.0)
make laws                   # both
cargo test -p eukhe-core --lib memory::chat::bridge
cargo test -p eukhe-core --lib memory::compactor::bridge

The local full gate (make ci-local, the nightly gate) does all of this. It requires Bend 2.0.38 and Lean 4.34.0, the releases that scripts/toolchain.env pins. The bridge test skips with a message when bend is not installed. In the full gate, EUKHE_REQUIRE_BEND=1 makes a missing bend fail the test.

The Bend model #

File What it holds
bend/optchat.bend The model: nodes, the view, due, push, fit, the sawtooth, the compaction view, the work queue.
bend/spec.bend What the laws speak of: Tiles, Merged, Summarized, Before, Prefix.
LAWS.bend Every law of eukhe, as open claims, one section per area.
PROOF.bend One proof per law, as Laws.<law>, and every lemma, one section per area.
bend/bridge.bend Runs the model on a scenario file. The bridge test builds it with bend bend/bridge.bend -o <binary>.
memory/chat/bridge.rs The bridge test.

The model keeps a node's index i in binary, lowest bit first. So the parent of a node is one step away: drop the lowest bit. All arithmetic of the merge order runs on these bits. The proof kernel has unary numbers, so this keeps the bounded proofs (law d) inside its fuel. The universal proofs read a bit list through its value (Bits.val): comparing, adding and shifting bit lists does the same to their values (PROOF.bend section agree).

The model uses lists, not arrays, and says why:

  • The view: each merge reads every pair to find the most due one, and removes one line from the middle. An array does the same work (a full read and a shift). The view has some hundred lines.
  • The built nodes: a binary trie per level, keyed by the bits of i. It is a sparse map that only grows; lookups take about 20 steps.
  • Numbers in binary: a list of about 20 bits. The kernel checks these fast, and the proofs use plain induction on them.
  • The proofs: every universal law is proven by induction on these lists (push_agrees_always inducts on the view as "the view one level up, then one line"). Bend 2.0.37's Array is a Type: it has one owner, so a state holding one could not be reused or compared in a law. It is indexed by a U32 that wraps around, and Base proves nothing about its reads and writes. An array model would need its own proven array theory and a proven bridge to these laws, for no less work per merge.

The model is a specification, not the production code. The Rust code is the production code. The bridge test runs the compiled model (bend/bridge.bend imports bend/optchat.bend, the module the laws speak of), so the laws are about the code that the bridge runs.

The model compares dues without division. For two pairs a and b, (T - last_a)/2^la > (T - last_b)/2^lb is the same as (T+1)·2^lb + e_b·2^la > (T+1)·2^la + e_a·2^lb, with e = last + 1. The Rust code compares (T + 1 - first)/2^l, the same order shifted by 2. The bridge test shows that the two orders agree.

The laws and their strength #

"Universal" means: proven for every input. "Bounded" means: proven by computation for the inputs named.

Law Gist Says Strength
view_covers §3 After any history of events, the view covers messages 0..T-1: each once, contiguous, oldest first, each line a tree node. Universal
cview_covers §4 The same for the compaction view. Universal
message_appends §3.2 When no batch runs, a message appends only its own line. The old view is a prefix of the new one. Universal
message_merges_nothing §3.2 And it merges nothing. Universal
merge_is_one_pair §3.1 A merge replaces two sibling lines with their built parent, or changes nothing. Universal
merge_covers §3.1 A merge keeps the coverage. Universal
push_agrees §3.2 With push's list length as the budget, the due order makes exactly the merges of Taelin's push, at every step. Bounded: t = 0..20,000 (the gist's own check)
push_agrees_always §3.1, §3.2 The same at every step t of any number of pushes. Universal
fit_reaches_budget §3.2 A batch ends at or under its budget, unless no pair can merge (unbuilt parents). Universal
fit_merges_built §3.2 A batch merges only into built parents. Universal
view_summarized §3 After any history, every merged line of the view is built. Only a message's own line can wait for its summary. Universal
cview_summarized §4 The same for the compaction view. Universal
last_merges_8_9 §3.2 At T = 10 (view 0+4, 4+4, 8+1, 9+1, budget 3) due from last merges 8-9, as push does. Bounded: the gist's example
first_merges_0_7 §3.2 Due from first merges 0-7. Bounded: the gist's example
first_is_not_last §3.2, §7.1 So first and last are not the same order. Bounded: the gist's example
first_leaves_push §3.2, §7.1 Due from first leaves push within 11 steps. Bounded: t = 0..10
context_ends_at_node §4 A compaction's view ends at its node, and has only built lines. Universal
context_is_prefix §4 A compaction's view is the start of the compaction view. Universal

push_agrees_always proves the gist's claim for every t (PROOF.bend section agree). Under push alone, Taelin's list is a bijective binary counter: with keep bits d_j (newest entry first), T messages are T = sum (1 + d_j)·2^j, and entry j is one line of level j + d_j. The proof writes push's list (pa.GE) and the view (pa.GV) as functions of these digits, and shows that every step takes both to the next count:

  • Push is the digit increment: a 0 absorbs, a 1 carries (pa.push_ge).
  • The view's lines start where push's states are (pa.same_lines).
  • The fit makes exactly the carry's merge (pa.step). If the lowest digit is 1, the view is the view of the higher digits one level up, plus the new line. Between lifted lines the due order is the same (T + 2 doubles), so the step is the lifted step (pa.fit_lift). If it is 0, the pair (line 0, the new line) is due exactly 1. Every older line ends at most 2^(l+1) - 2 messages before its view ends (pa.Good), so it is due less than 1 (pa.old_one). No later pair can merge: their levels differ by one. With push's length as the budget, this one merge is all the fit makes; when every digit is 1, push's list grows and the fit merges nothing.

The bounded law stays: its proof is a computation, independent of this one. The model also counts the steps where first agrees with push: 481 of 20,001, as the gist says (O.agree.count(O.First{}, 20001n)).

The kernel cannot run 20,001 steps in one check (it runs out of fuel), so PROOF.bend section push cuts the run into 20 chunks of 1,000 steps and one step. A lemma proven by induction joins the chunks. Each chunk starts from a written-out state and is checked by computation. Each join names the steps that are left as a literal (20001n, 19001n, …, 1001n), so the checker compares a sum with a literal at most 1,000 steps deep. A sum of 20 nested sums against 20001n makes the checker recurse 20,001 frames deep, and that overflows its JavaScript stack in some runs. On this machine bend PROOF.bend takes about 30 s and --verdict about 2 min 15 s.

What is trusted #

  • The laws are theorems about bend/optchat.bend, checked by Bend 2.0.38 and by its BendTT kernel (Lean 4.34.0).
  • That the Rust planner does the same is a finite differential test (the bridge), not a proof. It also trusts the Bend 2.0.38 compiler, its runtime, and clang, which build the bridge binary.
  • The bridge and the laws catch real mistakes. Each of these breaks was made on purpose and then removed:
    • Due from first in the model: last_merges_8_9 fails, and so does the proof of push_agrees_always.
    • Due without the shift (t for T + 1 in more_due), or with the two pairs' levels swapped in the cross comparison: the proof of push_agrees_always fails.
    • A budget of push's list length + 1 in agree.go: the proof of push_agrees_always fails.
    • Merging into an unbuilt parent in the model (mergeable without the built check): the proof of fit_merges_built fails.
    • Due from first in the Rust code: the bridge fails.
    • A Rust batch that merges down to the ceiling instead of the floor: the bridge fails.

The bridge #

memory/chat/bridge.rs makes scenarios with a fixed random seed. Each event goes to the Rust Chat and into a scenario file. The Bend binary reads the file. After each event, both print the view, the compaction view, the shrinking flag, the batch, the work queue, and the work queue while a turn waits (n=, the law need_first, docs/harness.md H2). At each batch and at the end, both print the whole rendered view (<chat> … </chat>), and for some nodes the rendered compaction view. The test compares the two outputs byte for byte. The compactor's line has a bridge of its own (memory/compactor/bridge.rs, docs/harness.md H1).

The scenarios:

  • 3,000 messages at the gist's sizes, with a compactor that is never late.
  • 4,000 messages at small sizes (6,000/3,000 and 2,000/1,000 bytes), so that the tree gets deep.
  • 1,500 and 2,500 messages with a late compactor: calls start, fail and end in any order, so batches stop above the floor and go on later.
  • 24 more runs with random sizes, budgets and job counts.

So the planner is compared at every T up to 4,000.

The rules #

Status: matched (the code does the rule), fixed (the code did not do the rule; a test now shows the fix), deliberate (a difference, with the reason).

§1 The log #

# Rule Code Secured by Status
1 main/YYYY-MM-DD.jsonl {i, kind, text, size, date}, tree/YYYY-MM-DD.jsonl {l, i, text, size}, view.json [l, i] pairs. memory/store.rs Store, MessageRecord, NodeRecord; memory/saved.rs save_view, load_view store::tests::appends_go_to_their_day_file_and_read_back_after_a_reload, saved::tests::a_saved_view_loads_back_unchanged matched (see D1, D2)
2 i is the permanent id. Files split by day. store.rs Store::open, Appender::append store::tests::a_gap_in_message_ids_is_an_error matched
3 Kinds user, tool, echo, work. memory/mod.rs Kind; durable/optchat/lines.rs optchat::tests::input_entries_are_logged_by_their_origin matched
4 Kind unii: the agent's replies. Kind::Eukhe (eukhe; old logs' talk still reads) memory::tests::old_talk_lines_read_as_eukhe renamed (D3)
5 Kind note: memories from before the chat. Kind::Note; memory/import.rs import_sessions import::tests::sessions_import_oldest_first_without_repeated_pastes fixed
6 Only append. Flush each write. Never edit or delete a line. store.rs Appender::append (one write, fsync) store::tests::a_torn_last_line_is_skipped_and_terminated matched
7 One process owns a chat. It holds a lock. memory/service/client.rs claim (chat/lock socket) service::tests::a_second_handle_is_a_client_and_takes_over, a_stale_socket_is_taken_over matched
8 Thoughts are shown, never logged. optchat/lines.rs assistant_lines optchat::tests::thoughts_are_never_logged_and_tool_output_is_clipped matched
9 A tool's output is clipped to head and tail, 30,000 characters in all. mod.rs CAP, cap_text; lines.rs tool_result_line memory::tests::cap_keeps_head_and_tail_within_the_cap, optchat::tests::thoughts_are_never_logged_and_tool_output_is_clipped matched
10 Other long text is never cut: it is logged as several messages in a row. mod.rs split_text; lines.rs entry_lines memory::tests::long_texts_split_into_messages_without_losing_anything, optchat::tests::long_user_texts_are_logged_as_several_messages matched

§2 The tree #

# Rule Code Secured by Status
11 node(0, i) is message i in at most 512 bytes; node(l, i) merges node(l-1, 2i) and node(l-1, 2i+1). It covers the 2^l messages from i·2^l and is named id+n. memory/view.rs Part; mod.rs NODE; service/actor.rs prepare Laws view_covers, merge_is_one_pair; chat::tests::zoom_opens_lines_and_messages matched
12 A source that fits in 512 bytes is its own node, with no model call. A short message stays word for word. Two short lines are joined by a newline. memory/chat.rs Step::input; actor.rs Actor::prepare chat::tests::a_nodes_input_is_its_message_or_its_children, service::tests::short_messages_are_their_own_lines matched
13 Each node is built once, logged, never built again. actor.rs built; chat.rs insert_node (leaves the queues) Bridge (the queue column); service::tests::deleting_the_saved_view_reproduces_the_identical_view matched

§3 The view #

# Rule Code Secured by Status
14 The view is a list of nodes that covers the whole chat, oldest first. view.rs View Laws view_covers, merge_covers matched
15 <chat>, one `id+n textline per node,`. view.rs View::render Bridge (the rendered views)
16 Newlines turned into spaces. view.rs flatten Bridge (each node text has a newline); view::tests::flatten_turns_every_newline_into_one_space (\r\n, \r) matched
17 No dates: the model calls date(id). View::render; optchat/tools.rs service::tests::dates_read_as_local_date_and_time matched
18 Only summaries, never a whole message. A message not summarized yet shows as (not summarized yet: zoom it). view.rs line_text; mod.rs PLACEHOLDER Law view_summarized; bridge; view::tests::placeholders_count_until_built matched
19 The merge order: the most due pair whose parent is built, due = (T - last)/2^l, oldest of equal pairs; repeat. view.rs View::fit, more_due Laws push_agrees_always, push_agrees, last_merges_8_9, fit_merges_built; bridge matched
20 With push's list length as budget, it makes push's merges at every step. View::fit Law push_agrees_always (universal: every t); law push_agrees (bounded, t ≤ 20,000) matched
21 Measuring from first is wrong: at T = 10 it merges 0-7, push merges 8-9. View::fit (uses last) Laws first_merges_0_7, first_is_not_last, first_leaves_push matched
22 Each new message appends its line. Nothing else changes. chat.rs Chat::grow Laws message_appends, message_merges_nothing; bridge matched
23 Past 128,000 bytes, one batch merges the most due pairs down to 64,000 bytes. mod.rs VIEW_CEILING, VIEW_FLOOR; chat.rs Chat::batch Law fit_reaches_budget; bridge matched
24 A batch merges only pairs whose parent is built. If it cannot reach 64 KB yet, it merges what it can at each new message until it does. Chat::grow, Chat::batch (shrinking) Laws fit_merges_built, fit_reaches_budget; bridge (late compactor); chat::tests::a_restart_keeps_a_shrinking_view_shrinking fixed (F1)
25 Save the view to view.json, load it at start. Never rebuild it from the log. actor.rs Actor::load, save_view; chat.rs Chat::from_loaded chat::tests::the_view_folds_from_the_logged_merges, a_restart_keeps_a_shrinking_view_shrinking; service::tests::deleting_the_saved_view_reproduces_the_identical_view deliberate (D1)
26 A call's input is [tools] [system prompt] [view] [new message]. optchat/request.rs transform, request_messages optchat::tests::a_request_joins_the_leading_user_texts_behind_the_view deliberate (D11)
27 Tools and system prompt never change: no dates, no state. memory/prompts.rs memory_system_layer; prompts/system_prompt.rs environment_section prompts::system_prompt::tests::chat_memory_prompt_leads_with_memory_and_ends_with_the_user_instructions matched (D4)
28 Between batches the view only grows at its end. A batch rewrites it once. Chat::grow Laws message_appends, merge_is_one_pair; bridge matched
29 The view goes in blocks of 4 lines. One cache mark on the last whole block, one on the request's end. mod.rs BLOCK; view.rs pieces; request.rs; memory/compactor.rs build_line view::tests::pieces_end_the_marked_piece_at_the_last_whole_block, compactor::tests::the_view_is_marked_at_its_last_whole_block matched
30 A call whose marked prefix another call is writing waits until that call's response starts. service/prefix_gate.rs; compactor.rs ResponseStarted; actor.rs spawn_build service::tests::a_waiting_compaction_goes_once_the_writers_response_starts, prefix_gate::tests::one_call_writes_a_new_prefix_and_the_others_wait_for_it fixed
31 No 1-hour entries, no keep-alive pings. memory/summarizer.rs (no cache retention set, no ping) — (an absence) matched

§4 Compactions #

# Rule Code Secured by Status
32 A compaction is a call with the turns' system prompt and tools (never called), then its view, then its task. request.rs (sets prompt.json); saved.rs CompactionPrompt; compactor.rs build_line service::tests::compactions_reuse_the_turns_prompt_and_see_built_lines_only matched
33 Its view: the chat's view merged further to 16-32 KB with the same sawtooth; merged down to 16 KB once, then the new lines appended; merged again past 32 KB or when the chat's view merges. mod.rs COMPACTION_CEILING, COMPACTION_FLOOR; chat.rs grow, batch, fit_compaction Laws cview_covers, cview_summarized; bridge matched
34 The compaction view ends at the node: the lines before the message, or for a merge the lines up to its last message. Only built lines. chat.rs Chat::context; view.rs render_built_until Laws context_ends_at_node, context_is_prefix; bridge fixed (F2)
35 The task texts, verbatim, with the 512-dash ruler. prompts.rs compress_task, merge_task, ruler prompts::tests::tasks_carry_the_ruler_and_the_whole_input matched
36 Over 512 bytes: the Too long: follow-up in the same conversation, with the line cut at 512 bytes. prompts.rs cut_follow_up; compactor.rs build_line prompts::tests::the_cut_falls_at_the_limit_without_splitting_characters, compactor::tests::over_long_lines_get_the_cut_and_the_shortest_cut_wins matched
37 At most 5 tries; keep the shortest line. mod.rs TRIES, cut; build_line Laws line_calls_at_most_tries, line_fits, line_takes_first_fit, line_stops_when_not_shorter, line_empty_is_a_try; the compactor bridge; compactor::tests::a_follow_up_not_shorter_ends_the_tries, a_line_at_the_limit_ends_the_retries deliberate (D9)
38 A failed call is tried again at the next message. actor.rs retry_failed, failed service::tests::a_failed_node_is_tried_again_only_for_a_waiting_turn, a_node_that_keeps_failing_is_built_from_its_input; law fail_cap_builds matched (D5)
39 Up to 8 calls at once. mod.rs JOBS; chat.rs Chat::candidates Bridge (the queue column, with 1 to 8 jobs) matched
40 A message's node starts once fewer than 8 lines before it are unbuilt. A merge starts once both halves are built. Chat::candidates, note_ready_parent Bridge (the queue columns); law need_first matched (D5)
41 A compaction's view stops at the first unbuilt line. view.rs render_built_until Law context_ends_at_node; bridge matched
42 Keep ready nodes in queues. Never scan the tree for work. chat.rs ready (a queue, filled when a node is built) Bridge (the queue order); code review: candidates reads the view's lines and the queue only matched
43 The model: a cheap one. summarizer.rs configured_memory_model summarizer::tests deliberate (D6)

§5 The prompt #

# Rule Code Secured by Status
44 One system prompt for turns and compactions. The user's instructions follow it. prompts.rs memory_system_layer; prompts/system_prompt.rs prompts::system_prompt::tests::chat_memory_prompt_leads_with_memory_and_ends_with_the_user_instructions matched (D4)
45 The prompt verbatim, Unii renamed. The paragraph on computers is dropped when the agent has no device tools. prompts.rs INTRO, VIEW_DOC, TURNS, COMPACTIONS; the gist's text in memory/prompts/gist_prompt.txt prompts::tests::the_system_prompt_is_the_specs_with_only_the_documented_deviations fixed (F3)

§6 A turn #

# Rule Code Secured by Status
46 If a call is running, hand m to it between tool calls and log it as user. Harness steering; lines.rs user_input_line optchat::tests::a_message_sent_during_a_call_joins_it_between_tool_calls matched
47 Else wait until every message before m is summarized, then call in a fresh session. request.rs transform, pin; actor.rs view waiters (settled_render), deadline_view service::tests::a_settled_view_never_shows_a_placeholder_while_another_client_appends, a_turn_gets_the_view_by_its_deadline_when_the_model_never_answers; laws settle_reached, need_first deliberate (D10)
48 Log each reply, tool call and result as it happens. optchat/logger.rs optchat::tests::a_crash_between_append_and_cursor_logs_no_line_twice matched
49 Render the view before logging the new message. The message goes whole after it. logger.rs log_conversation; request.rs optchat::tests::a_run_sends_the_pinned_view_and_its_own_messages_and_logs_them deliberate (D11)
50 Nothing carries over between turns. request.rs request_messages Same test matched
51 Per-turn state goes after the view, never in the system prompt. request.rs request_messages optchat::tests::a_request_joins_the_leading_user_texts_behind_the_view matched
52 zoom(id, n): the description verbatim; n a power of 2, id a multiple of n. prompts.rs ZOOM_TOOL_DESCRIPTION; chat.rs Chat::zoom; optchat/tools.rs chat::tests::zoom_opens_lines_and_messages, optchat::tests::zoom_and_date_keep_their_schemas_and_read_the_chat matched
53 zoom(id, 1) gives the message whole, with its images. saved.rs save_images, load_images (chat/images/); actor.rs zoom; tools.rs zoom_result service::tests::a_message_zoomed_whole_comes_with_its_images, optchat::tests::the_zoom_tool_gives_a_message_with_its_images fixed
54 zoom("Name") gives an agent's whole chat; its steps stay in its own log. tools.rs agent_chat; lines.rs chat_text optchat::tests::zoom_by_name_gives_an_agents_whole_chat fixed
55 A long message comes in pages. Rule 10: each page is one message. optchat::tests::long_user_texts_are_logged_as_several_messages matched
56 date(id): the description verbatim. prompts.rs DATE_TOOL_DESCRIPTION; tools.rs service::tests::dates_read_as_local_date_and_time matched
57 A subagent is a fresh call: the view, then its task. request.rs (MemoryRole::Subagent); prompts.rs subagent_system_layer optchat::tests::a_subagent_pins_one_view_for_its_conversation_and_logs_nothing matched
58 Its final reply comes back as one work message, [Name] report. The master never waits or polls. lines.rs report_line; the prompt's TURNS optchat::tests::agent_replies_are_logged_as_work_and_owed_rows_are_not matched

§7 Mistakes to avoid #

# Mistake Avoided by Secured by
59 1. Age from a pair's first message. Rule 19 (last) Laws first_is_not_last, first_leaves_push, push_agrees_always
60 2. Merging at every message. Rules 22-24 Laws message_appends, fit_reaches_budget; bridge
61 3. Rebuilding the view per turn or at a restart. Rule 25 chat::tests::a_restart_keeps_a_shrinking_view_shrinking
62 4. K lines per level. Rule 23 (a byte budget) Law fit_reaches_budget
63 5. Whole messages in the view, or the start of an unsummarized one. Rules 18, 41 Laws view_summarized, context_ends_at_node
64 6. A compaction without context, or with its own system prompt. Rules 32-34 Rule 32-34 tests and laws
65 7. Trusting the model to count bytes. Rules 35-37 Rule 35-37 tests
66 8. Lines under 512 bytes. NODE = 512 chat::tests::free_nodes_need_no_model
67 9. Logging thoughts. Rule 8 Rule 8 test
68 10. Dates or state in the system prompt or tools. Rules 27, 51 Rule 27, 51 tests
69 11. Carrying a conversation across turns. Rule 50 Rule 50 test
70 12. Two writers on one log, or writes without a flush. Rules 6, 7 Rule 6, 7 tests
71 13. Scanning the tree for work. Rule 42 Bridge

Fixes #

  • F1. A batch that stopped above 64 KB went on at the next messages, but a restart lost that state. The view then only appended until it passed 128 KB again, so it differed from the live view. Now a batch runs at every message while the view is shrinking, and each batch writes its tree/ line, also when it merges nothing: {merge, open, t}. open says the view is still above its floor; t is the number of messages. A restart reads open from the last line. Old lines have no open and no t, and still load (open false).
  • F2. A compaction's view took the lines that start before the node. It now takes the lines that end at or before the node (gist: "the lines before the message, or for a merge the lines up to the merge's last message"). With the tree as it is built, both give the same lines (a line that crosses the node's end would contain the unbuilt node). The bridge shows this.
  • F3. The prompt said "Spawn subagents when needed". The gist says "Do the user's tasks yourself, with your tools, … Use subagents only when the user asks for them." The kind list had no note. The tool list had no images for zoom(id, 1) and no zoom("Name").

Deliberate differences #

  • D1. The view is folded from logged merges. The gist saves the view to view.json and loads it. eukhe also writes view.json after every change, but at start it folds the view from the messages and the merge lines in tree/, and checks view.json against the fold. The fold replays the exact merges of the live view, in their order. So it gives the same parts, and the same bytes, as the live view. It does not compute the view again by another rule, which is what the gist warns against. The merge lines are append-only, as the log is; view.json is replaced on each change and can be lost in a crash. A lost or stale view.json gives the same view (service::tests::deleting_the_saved_view_reproduces_the_identical_view). A chat from before the merge lines imports its view.json once, as merges.
  • D2. Extra fields. A message line can carry key (an idempotent append across crashes). tree/ also holds the merge lines (D1). The gist's fields are all there, unchanged.
  • D3. eukhe, not unii. The gist says to rename Unii to the agent's name, so the agent's replies are kind eukhe, and the prompt's kind list says eukhe. Lines logged before the rename say talk; they read as eukhe. Old tree lines keep their talk: text, since nodes are never rebuilt.
  • D4. Static layers. eukhe's own static tool notes sit between the memory prompt and the user's instructions. They do not change, so the cache prefix holds. The working directory is part of a session's set-up and does not change in a chat's sessions.
  • D5. A waiting turn comes first. While a turn waits for the view (rule 47), only the view's unbuilt lines start: the ready merges wait until the view settles, and a call running for any other node stops when the turn starts to wait (no failure; laws need_first, need_first_running). A failed node is also tried every 10 seconds (RETRY); without this, a turn waits for a message that does not come. The third failed call of a node builds it from its input cut to 512 bytes (FAILURES, law fail_cap_builds), so the view settles after a bounded number of compactor outcomes (laws settle_*, docs/harness.md H2). The wait never ends with an error.
  • D6. The model. The gist names UniiChat's model (Claude Haiku). eukhe uses the model in the user's settings (memory.model, else the auxiliary model).
  • D7. The compaction view at start. The compaction view is not logged. At start it is the view merged down to 16 KB once, as the gist starts it. This changes only which prefix the first compactions read from the cache.
  • D8. A crash between a message and its batch line. While the view shrinks, every message writes a batch line, so a restart sees a missing line and runs that batch. When the view is not shrinking and a message passes 128 KB, the batch line can be lost in a crash. Then the batch runs at the next message instead. A restart never runs a batch on its own, because the live chat also waits for the next message.
  • D9. A line is cut, not trusted to shrink. The gist keeps the shortest of 5 tries, whatever its length. eukhe stops at a follow-up that is not shorter than the best line (a measured local model wrote the same 1,300-byte line every time), takes an empty or thinking-only reply as the last try (asking again asks the same; a measured model thought 12,000 tokens and wrote nothing), and cuts the best line, else the input, to 512 bytes at a character boundary. So every line is 1 to 512 bytes (docs/harness.md H1). A model call past 10 minutes fails (CALL_DEADLINE).
  • D10. A turn waits for its view at most memory.settleTimeoutMs. The gist waits until every message before m is summarized. After memory.settleTimeoutMs (default 20 s) a turn takes the view as it is: each line not summarized yet shows its input (the message, labeled) cut to 512 bytes, never the placeholder and never an error. The line is summarized later as usual; the next turn sees its summary (docs/harness.md H2.3).
  • D11. A turn is marked "Turn:". The gist marks a compaction with "Compaction:" and puts a turn's new message after the view with no mark. A model must then tell a turn by the missing mark. One model took a bare user message for a compaction input and replied with a summary line. eukhe puts the line Turn: the new message follows. Answer it as Eukhe, with your tools; do not summarize it. before the new message, and the prompt's opening names both marks (memory/prompts.rs INTRO, TURN_TASK). The line goes to the model only. The chat logs the user's words alone. A subagent's first message has no mark: its task follows the view.