Chat memory #
Source of truth: Victor Taelin's OptChat gist ("UniiChat: one chat that never ends", 91837951, revision 2026-10-08). This page maps each gist rule to Rust code and the law or test securing it.
- Rust:
crates/eukhe-core/src/memory/,crates/eukhe-core/src/durable/optchat/; paths below start atcrates/eukhe-core/src/. - Bend model of the pure core (view, merge order, sawtooth, compaction view, work queue):
bend/optchat/. LAWS.bendstates the laws;PROOF.bendproves them.- The planner runs as the compiled model: stepper
bend/ffi_optchat.bend(entry 422), wrappereukhe-bendrules/optchat.rsPlanner. Rust holds no copy of it and no bridge checks one.
Use #
- Store:
~/.eukhe/chat/, one append-only log. - Kinds:
user,talk,tool,echo,work(subagent or background report). - Compactor writes one-line summaries into a binary tree over the log.
- Summary target 512 bytes; longer: retried while shorter, max 5 tries; shortest cut to 512 bytes.
- Failed summary: retried at next message and every 10 s while a turn waits; 3rd failure: line = message cut to 512 bytes.
- Idle chat: no model call.
- Turn waits for view summaries max
memory.settleTimeoutMs. - View = context each root turn starts from. Past 128 KB, one batch merges it to 64 KB.
- View saved to
view.jsonon each change: prompt cache valid across restarts. - Cache mark on last whole block of 4 lines.
- Compaction calls: own view, 16-32 KB.
| Command | Effect |
|---|---|
/memory, /memory view |
Status and every view line |
/memory model |
The /model hub at the memory role: compaction model and reasoning effort (saved as memory.model, memory.thinking, default medium) |
- Status bar:
mem N/128K. - Each compactor change: a
memory:row. Summary text at full detail only.
Check it #
bend PROOF.bend # ALL PROOFS CHECK
bend PROOF.bend --verdict # same, BendTT kernel (needs Lean 4.34.0)
make laws # both
cargo test -p eukhe-bend rules::optchat
cargo test -p eukhe-core --lib memory
make ci-local(local full gate, nightly gate) runs all of this; needs Bend 2.0.39, Lean 4.34.0 (pinned inscripts/toolchain.env).make bend-coreregenerates the compiled core (crates/eukhe-bend/c/core.c) after a change underbend/;make bend-core-checkfails when it is stale.
The Bend model #
| File | Holds |
|---|---|
bend/optchat/ |
Model: nodes, view, due, push, fit, sawtooth, compaction view, work queue; open.bend the open of a loaded chat (built nodes, ready merges, missed batch, compaction view). |
bend/ffi_optchat.bend |
Stepper entry (id 422): the chat's planner, its state the model's chat in the Bend heap. |
bend/spec.bend |
Law vocabulary: Tiles, Merged, Summarized, Before, Prefix. |
LAWS.bend |
Imports only; laws/<area>.bend one area's laws as open claims, laws/base.bend shared spec helpers. |
PROOF.bend |
Imports only; proof/<area>/<law>.bend one law's proof, proof/<area>/base/ and proof/base/ shared lemmas. |
eukhe-bend rules/optchat.rs |
Planner: opens the stepper, spells each event and query, reads each reply. |
- Node index
i: binary, lowest bit first; parent = drop lowest bit. All merge-order arithmetic on bits. Kernel numbers are unary, so this keeps bounded proofs (law d) in fuel. - Universal proofs read bit lists by value (
Bits.val); compare, add, shift agree with values (proof/optchat/push_agrees_always.bend).
Lists, not arrays:
-
View: each merge reads every pair for the most due and removes one middle line; an array does the same work (full read, shift). View: ~100s of lines.
-
Built nodes: binary trie per level keyed by bits of
i; sparse, grow-only; lookup ~20 steps. -
Numbers: ~20-bit lists; fast in kernel; plain induction.
-
Proofs: every universal law by induction on these lists (
push_agrees_alwaysinducts on view as "view one level up, then one line"). Bend 2.0.37Arrayis a single-ownerType: a state holding one cannot be reused or compared in a law; indexed by wrappingU32; Base proves nothing about its reads/writes. An array model needs its own proven array theory, no less work per merge. -
The model is the running code:
bend/ffi_optchat.bendimportsbend/optchat/, the modules the laws speak of, and the compiled core runs it, so the laws cover the planner that runs. -
Due compare without division:
(T - last_a)/2^la > (T - last_b)/2^lb⇔(T+1)·2^lb + e_b·2^la > (T+1)·2^la + e_a·2^lb,e = last + 1.
The laws and their strength #
Universal: every input. Bounded: computed for named inputs.
| Law | Gist | Says | Strength |
|---|---|---|---|
view_covers |
§3 | After any event history, view covers 0..T-1: each once, contiguous, oldest first, each line a tree node. |
Universal |
cview_covers |
§4 | Same for compaction view. | Universal |
message_appends |
§3.2 | No batch: a message appends only its line; old view is prefix of new. | Universal |
message_merges_nothing |
§3.2 | And merges nothing. | Universal |
merge_is_one_pair |
§3.1 | Merge replaces two sibling lines with their built parent, or no change. | Universal |
merge_covers |
§3.1 | Merge keeps coverage. | Universal |
push_agrees |
§3.2 | Budget = push's list length: due order makes exactly Taelin's push merges each step. | Bounded: t = 0..20,000 (gist's check) |
push_agrees_always |
§3.1, §3.2 | Same at every t of any number of pushes. | Universal |
fit_reaches_budget |
§3.2 | Batch ends at or under budget unless no pair can merge (unbuilt parents). | Universal |
fit_merges_built |
§3.2 | Batch merges only into built parents. | Universal |
view_summarized |
§3 | After any history every merged view line is built; only a message's own line waits for its summary. | Universal |
cview_summarized |
§4 | Same for compaction view. | Universal |
last_merges_8_9 |
§3.2 | T = 10 (view 0+4, 4+4, 8+1, 9+1, budget 3): due from last merges 8-9, as push. |
Bounded: gist's example |
first_merges_0_7 |
§3.2 | Due from first merges 0-7. |
Bounded: gist's example |
first_is_not_last |
§3.2, §7.1 | first and last orders differ. |
Bounded: 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 | Compaction view ends at its node; only built lines. | Universal |
context_is_prefix |
§4 | Compaction's view is a prefix of the compaction view. | Universal |
The planner as it runs (laws/optchat.bend section S, the stepper bend/ffi_optchat.bend; every event order, every input):
| Law | Says | Excludes |
|---|---|---|
optchat_step_is_model |
Opened from any loaded chat and sent any events and queries in any order, the stepper answers each as the model run from the opened chat. | Replies that drift from the model after some event order; an open that starts the model elsewhere. |
optchat_entry_answers |
Each event or query, spelled: a message runs OStep.step's batch (merges, view after, still shrinking); a build, start, free are OStep.step's events; a wait stops exactly the busy nodes that are not unbuilt view lines (St.wait); cands, cands.need, ctx, the busy count move nothing. |
A batch lost or invented, a node started twice, a merge offered while a turn waits, a needed call stopped, a compaction shown an unbuilt line, a query that moves the chat. |
optchat_open_is_model |
The open loads sizes, view, T, built nodes; the compaction view is the view fitted to its floor; the ready merges are those the built nodes note; no call runs; a missed batch runs as a message's batch and is answered. | A missed batch lost or run twice, a batch at an open that missed none, an unfitted compaction view, a lost ready merge. |
optchat_open_ready |
The ready merges after an open are exactly the parents of built pairs that are not built. | A merge never started after a restart; a merge before both halves; a built node built again. |
optchat_entry_covers |
Opened from a view that tiles the chat, both views tile messages 0..T after any inputs, garbage included. |
A message lost, doubled or out of order in either view after some event order. |
optchat_bits_of |
A wire index's bits have its value and no high zero bit. | A misread index; a padded one the built-node tries file under another key. |
optchat_entry_reads, optchat_entry_malformed_exact, optchat_entry_open_reads, optchat_entry_open_malformed_exact, optchat_entry_shut, optchat_entry_example |
The entry over the laws' own reader; any other input gets its exact error form (optchat: malformed event, malformed open, the open was refused) and keeps the chat. |
Garbage read as an event (a message that runs a batch, a wait that stops calls), a default chat opened for garbage, a refused stepper that answers. |
push_agrees_always (proof/optchat/push_agrees_always.bend): under push alone Taelin's list is a bijective binary counter. Keep bits d_j (newest first): T = sum (1 + d_j)·2^j; entry j = one line of level j + d_j. Push's list (pa.GE) and view (pa.GV) are functions of the digits; each step takes both to the next count:
-
Push = digit increment: 0 absorbs, 1 carries (
pa.push_ge). -
View lines start at push's states (
pa.same_lines). -
Fit makes exactly the carry's merge (
pa.step). Lowest digit 1: view = higher digits' view one level up + new line; due order between lifted lines is the same (T + 2 doubles), so step = lifted step (pa.fit_lift). Lowest digit 0: pair (line 0, new line) is due exactly 1; every older line ends at most2^(l+1) - 2messages before its view ends (pa.Good), so due < 1 (pa.old_one); later pairs cannot merge (levels differ by one). With push's length as budget this one merge is all; all digits 1: push's list grows, fit merges nothing. -
Bounded law kept: independent computed proof.
firstagrees with push at 481 of 20,001 steps, as gist says (O.agree.count(O.First{}, 20001n)). -
Kernel runs out of fuel on 20,001 steps, so
proof/optchat/push_agrees.bendsplits into 20 chunks of 1,000 steps + 1 step, joined by an induction lemma. Each chunk starts from a written-out state, checked by computation. Each join names remaining steps as a literal (20001n,19001n, …,1001n): checker compares a sum with a literal ≤ 1,000 steps deep. 20 nested sums vs20001nrecurse 20,001 frames and overflow the checker's JavaScript stack in some runs. -
Timing (this machine):
bend PROOF.bend~30 s;--verdict~2 min 15 s.
What is trusted #
- Laws: theorems about
bend/optchat/and the stepper entrybend/ffi_optchat.bend, checked by Bend 2.0.39 and its BendTT kernel (Lean 4.34.0). - Trusted, not proven: the Bend 2.0.39 compiler and runtime, clang, the C shim (
eukhe-bendc/shim.c,src/ffi.rs), the RustWriter/Readercodec; the Rust log replay that feeds the open (the view folded from the merge lines, the missed-batch flag:chat.rsChat::from_loaded). - Deliberate breaks, each caught, then removed:
- Due from
firstin model:last_merges_8_9andpush_agrees_alwaysproof fail. - Due without shift (
tforT + 1inmore_due), or pair levels swapped in cross comparison:push_agrees_alwaysproof fails. - Budget push length + 1 in
agree.go:push_agrees_alwaysproof fails. - Merge into unbuilt parent in model (
mergeablewithout built check):fit_merges_builtproof fails. - Wrong steppers (one change to
bend/ffi_optchat.bendorbend/optchat/open.bend; a concrete instance of the law holds on the shipped entry and fails on the wrong one): a batch that ran answered as none (optchat_step_is_model); a wait that stops no call, the busy count counting the ready merges, an index read one off (optchat_entry_answers); a waiting turn offered the ready merges (optchat_entry_example); garbage read as a message (optchat_entry_malformed_exact); tags 5 and 6 read as one (optchat_entry_reads); a refused open that answers (optchat_entry_shut); an open that forgets the ready merges (optchat_open_ready); a missed batch never run, a restart that forgets the view shrinks, a compaction view that starts unfitted (optchat_open_is_model).
- Due from
The stepper #
Chat (memory/chat.rs) opens the planner from its loaded log (Chat::from_loaded: sizes, T, built nodes with their sizes, the view folded from the merge lines, whether the last batch is open or missing), then sends each event as it happens and each query it needs, and does what each reply says:
| Rust | Request | Reply |
|---|---|---|
Chat::push_message |
message | the batch it ran (logged as a tree/ line), the view after it, still shrinking |
Chat::insert_node |
node built, its size | done |
Chat::mark_busy, Chat::release |
call started, freed | done |
Chat::hold_for_turn |
a turn starts to wait | the calls to stop |
Chat::candidates(Demand) |
nodes to start (Background, Turn) |
nodes, in the gist's order |
Chat::context |
a compaction's view of a node | its lines |
Chat::busy_count |
calls running | a count |
Rust keeps the texts and renders them; its View mirrors the planner's last view for rendering and sizes. It decides nothing.
Host tests: eukhe_bend::rules::optchat::tests::the_planner_runs_in_the_core (the core is reached: an open with a missed batch, each event and query); chat::tests::a_missed_batch_runs_at_the_load_as_it_ran_live, a_restart_keeps_a_shrinking_view_shrinking, the_view_folds_from_the_logged_merges; service::tests::turns_wait_while_calls_finish_in_any_order (three handles append 360 messages while the compactor's calls are answered newest first in bursts and two turns wait: every view a turn gets is settled, a built merge is merged by the next batch, a reload from the logs alone gives the live view).
Cost per call (this machine, test profile; a chat of 4,000 messages grown with a compactor never late: 7,994 built nodes, view 362 lines of 300 bytes; median of 20 calls, three runs): open of the loaded chat 165-171 ms; a message 168-176 µs, one that runs a batch (108 KB to the 64 KB floor) about 152-154 ms; a node built 16-18 µs; a call started or freed 1-2 µs; the nodes to start 139-153 µs (either demand); a compaction's view 82-122 µs; a wait 348-491 µs; the busy count about 1 µs. The batch and the open run the model's fit (bend/optchat/fit.bend), one scan of the view per merge.
The rules #
Status: matched (code does the rule), fixed (code did not; a test shows the fix), deliberate (difference, reason given).
§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 (D1, D2) |
| 2 | i 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: agent replies. |
Kind::Eukhe (eukhe; old talk still reads) |
memory::tests::old_talk_lines_read_as_eukhe |
renamed (D3) |
| 5 | Kind note: pre-chat memories. |
Kind::Note; memory/import.rs import_sessions |
import::tests::sessions_import_oldest_first_without_repeated_pastes |
fixed |
| 6 | Append only; flush each write; never edit/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, holding 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 shown, never logged. | optchat/lines.rs assistant_lines |
optchat::tests::thoughts_are_never_logged_and_tool_output_is_clipped |
matched |
| 9 | Tool output clipped to head + tail, 30,000 characters total. | 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 never cut: logged as consecutive messages. | 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) = message i in ≤ 512 bytes; node(l, i) merges node(l-1, 2i), node(l-1, 2i+1); covers 2^l messages from i·2^l; 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 | Source ≤ 512 bytes is its own node, no model call; short message stays verbatim; two short lines joined by 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 built once, logged, never rebuilt. | actor.rs built; chat.rs insert_node (tells the planner; bend/optchat/step.bend build.go drops it from the queues) |
Law optchat_step_is_model (a build moves as OStep.step); service::tests::deleting_the_saved_view_reproduces_the_identical_view |
matched |
§3 The view #
| # | Rule | Code | Secured by | Status |
|---|---|---|---|---|
| 14 | View: node list covering 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, render_parts |
chat::tests::zoom_opens_lines_and_messages, service::tests::short_messages_are_their_own_lines |
| 16 | Newlines → spaces. | view.rs flatten |
view::tests::flatten_turns_every_newline_into_one_space (\n, \r\n, \r) |
matched |
| 17 | No dates; model calls date(id). |
View::render; optchat/tools.rs |
service::tests::dates_read_as_local_date_and_time |
matched |
| 18 | Only summaries, never whole messages; unsummarized: (not summarized yet: zoom it). |
view.rs line_text; mod.rs PLACEHOLDER |
Law view_summarized; view::tests::placeholders_count_until_built |
matched |
| 19 | Merge order: most due pair with built parent, due = (T - last)/2^l, oldest of equals; repeat. |
bend/optchat/fit.bend fit, more_due (compiled core) |
Laws push_agrees_always, push_agrees, last_merges_8_9, fit_merges_built, optchat_step_is_model |
matched |
| 20 | Budget = push's list length: push's merges every step. | bend/optchat/fit.bend fit |
Law push_agrees_always (universal, every t); law push_agrees (bounded, t ≤ 20,000) |
matched |
| 21 | From first is wrong: T = 10 merges 0-7, push merges 8-9. |
bend/optchat/fit.bend more_due (uses last) |
Laws first_merges_0_7, first_is_not_last, first_leaves_push |
matched |
| 22 | New message appends its line; nothing else changes. | bend/optchat/step.bend msg; chat.rs Chat::push_message |
Laws message_appends, message_merges_nothing, optchat_entry_answers |
matched |
| 23 | Past 128,000 bytes one batch merges most due pairs down to 64,000 bytes. | mod.rs VIEW_CEILING, VIEW_FLOOR (sent at the open); bend/optchat/step.bend St.batch, msg |
Laws fit_reaches_budget, optchat_entry_answers |
matched |
| 24 | Batch merges only built-parent pairs; short of 64 KB, merges what it can each new message until reached. | bend/optchat/step.bend St.batch (shrinking); bend/optchat/open.bend (a restart keeps it) |
Laws fit_merges_built, fit_reaches_budget, optchat_open_is_model; chat::tests::a_restart_keeps_a_shrinking_view_shrinking, a_missed_batch_runs_at_the_load_as_it_ran_live |
fixed (F1) |
| 25 | Save view to view.json, load at start; never rebuild from log. |
actor.rs Actor::load, save_view; chat.rs Chat::from_loaded (folds the view, opens the planner from it) |
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, turns_wait_while_calls_finish_in_any_order; law optchat_open_is_model |
deliberate (D1) |
| 26 | Call input: [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 view grows only at end; a batch rewrites it once. | bend/optchat/step.bend msg |
Laws message_appends, merge_is_one_pair, optchat_entry_answers |
matched |
| 29 | View in blocks of 4 lines; cache marks on last whole block and request 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 | Call whose marked prefix another call is writing waits until that 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, no ping) |
— (absence) | matched |
§4 Compactions #
| # | Rule | Code | Secured by | Status |
|---|---|---|---|---|
| 32 | Compaction: turns' system prompt and tools (never called), its view, 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: chat view merged further to 16-32 KB, same sawtooth; merged to 16 KB once, then new lines appended; re-merged past 32 KB or when the chat view merges. | mod.rs COMPACTION_CEILING, COMPACTION_FLOOR; bend/optchat/step.bend msg.cview, fitc; bend/optchat/open.bend (the view merged to 16 KB at the open) |
Laws cview_covers, cview_summarized, optchat_entry_covers, optchat_open_is_model |
matched |
| 34 | Compaction view ends at the node: lines before the message, or for a merge lines up to its last message; built lines only. | bend/optchat/ctx.bend ctx; chat.rs Chat::context (renders it) |
Laws context_ends_at_node, context_is_prefix, optchat_entry_answers |
fixed (F2) |
| 35 | 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: Too long: follow-up in the same conversation; 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 | Max 5 tries; keep 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; compactor::tests::a_follow_up_not_shorter_ends_the_tries, a_line_at_the_limit_ends_the_retries |
deliberate (D9) |
| 38 | Failed call retried at 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 | Max 8 concurrent calls. | mod.rs JOBS (sent at the open); bend/optchat/queue.bend cands |
Laws need_progress, optchat_entry_answers |
matched |
| 40 | Message node starts once < 8 lines before it are unbuilt; merge starts once both halves built. | bend/optchat/queue.bend cands; bend/optchat/step.bend ready.note |
Laws need_first, optchat_open_ready, optchat_entry_answers |
matched (D5) |
| 41 | Compaction view stops at first unbuilt line. | bend/optchat/ctx.bend ctx |
Law context_ends_at_node |
matched |
| 42 | Ready nodes in queues; never scan the tree for work. | bend/optchat/state.bend St.ready (queue, filled on build: ready.note) |
Law optchat_open_ready; code review: cands reads only view lines and the queue |
matched |
| 43 | 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; user instructions follow. | 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 | Prompt verbatim, Unii renamed; computers paragraph dropped without device tools. | prompts.rs INTRO, VIEW_DOC, TURNS, COMPACTIONS; gist 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 | Call running: hand m to it between tool calls; log 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; 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, result as it happens. | optchat/logger.rs |
optchat::tests::a_crash_between_append_and_cursor_logs_no_line_twice |
matched |
| 49 | Render view before logging the new message; 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 after the view, never in system prompt. | request.rs request_messages |
optchat::tests::a_request_joins_the_leading_user_texts_behind_the_view |
matched |
| 52 | zoom(id, n): description verbatim; n power of 2, id 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): whole message with 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"): 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 | Long message comes in pages. | Rule 10: each page one message. | optchat::tests::long_user_texts_are_logged_as_several_messages |
matched |
| 56 | date(id): description verbatim. |
prompts.rs DATE_TOOL_DESCRIPTION; tools.rs |
service::tests::dates_read_as_local_date_and_time |
matched |
| 57 | Subagent: fresh call, view then 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 | Final reply returns as one work message, [Name] report; master never waits or polls. |
lines.rs report_line; 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 pair's first message. | Rule 19 (last) |
Laws first_is_not_last, first_leaves_push, push_agrees_always |
| 60 | 2. Merging every message. | Rules 22-24 | Laws message_appends, fit_reaches_budget, optchat_entry_answers |
| 61 | 3. Rebuilding view per turn or at restart. | Rule 25 | chat::tests::a_restart_keeps_a_shrinking_view_shrinking |
| 62 | 4. K lines per level. | Rule 23 (byte budget) | Law fit_reaches_budget |
| 63 | 5. Whole messages in view, or start of an unsummarized one. | Rules 18, 41 | Laws view_summarized, context_ends_at_node |
| 64 | 6. Compaction without context, or with 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 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 unflushed writes. | Rules 6, 7 | Rule 6, 7 tests |
| 71 | 13. Scanning the tree for work. | Rule 42 | Law optchat_open_ready; code review of cands |
Fixes #
- F1. Batch stopped above 64 KB continued at next messages, but a restart lost that state (view only appended until 128 KB again; differed from live). Now: batch runs every message while shrinking; each batch writes a
tree/line, also when merging nothing:{merge, open, t}(open: view above floor;t: message count). Restart readsopenfrom last line. Old lines withoutopen/tload withopenfalse. - F2. Compaction view took lines starting before the node; now lines ending at or before it (gist: "the lines before the message, or for a merge the lines up to the merge's last message"). With the tree as built both give the same lines (a line crossing the node's end contains the unbuilt node); law
context_ends_at_nodepins the end. - F3. Prompt said "Spawn subagents when needed"; gist: "Do the user's tasks yourself, with your tools, … Use subagents only when the user asks for them." Kind list lacked
note; tool list lacked images forzoom(id, 1)andzoom("Name").
Deliberate differences #
-
D1. View folded from logged merges.
view.jsonwritten after every change; at start the view is folded from messages andtree/merge lines and checked againstview.json. Fold replays the live view's exact merges in order: same parts, same bytes; no recompute by another rule (gist's warning). Merge lines append-only;view.jsonreplaced per change, can be lost in a crash; lost/staleview.jsongives the same view (service::tests::deleting_the_saved_view_reproduces_the_identical_view). Pre-merge-line chat imports itsview.jsononce, as merges. -
D2. Extra fields. Message line may carry
key(idempotent append across crashes).tree/also holds merge lines (D1). All gist fields unchanged. -
D3.
eukhe, notunii. Gist: rename Unii to the agent's name; replies are kindeukhe, prompt's kind list sayseukhe. Pre-renametalklines read aseukhe; old tree lines keeptalk:text (nodes never rebuilt). -
D4. Static layers. eukhe's static tool notes sit between memory prompt and user instructions; unchanging, so cache prefix holds. Working directory: fixed per session set-up, unchanged across a chat's sessions.
-
D5. Waiting turn first. While a turn waits (rule 47) only the view's unbuilt lines start; ready merges wait for settle; calls for other nodes stop when the wait starts (no failure; laws
need_first,need_first_running). Failed node also retried every 10 s (RETRY), else a turn waits for a message that never comes. 3rd failed call builds the node from its input cut to 512 bytes (FAILURES, lawfail_cap_builds): view settles after bounded compactor outcomes (lawssettle_*, docs/harness.md H2). Wait never errors. -
D6. Model. Gist: Claude Haiku. eukhe:
memory.model, else auxiliary model. -
D7. Compaction view at start. Not logged; at start = view merged to 16 KB once, as gist. Affects only which prefix first compactions read from cache.
-
D8. Crash between message and batch line. Shrinking: every message writes a batch line; restart sees the gap and runs the batch. Not shrinking and a message passes 128 KB: crash can lose the batch line; batch runs at next message. Restart never runs a batch alone (live chat also waits for next message).
-
D9. Lines cut, not trusted to shrink. Gist keeps shortest of 5 tries at any length. eukhe:
- stops at a follow-up not shorter than best (measured local model repeated a 1,300-byte line);
- empty or thinking-only reply = last try (measured model thought 12,000 tokens, wrote nothing);
- cuts best line, else input, to 512 bytes at a character boundary.
Every line 1-512 bytes (docs/harness.md H1). Model call past 10 min fails (
CALL_DEADLINE). -
D10. Turn waits for its view max
memory.settleTimeoutMs. Gist waits until every prior message is summarized. Aftermemory.settleTimeoutMs(default 20 s) the turn takes the view as is: each unsummarized line shows its input (labeled message) cut to 512 bytes; never placeholder, never error. Summarized later; next turn sees it (docs/harness.md H2.3). -
D11. Turn marked "Turn:". Gist marks compactions "Compaction:" and leaves turns unmarked; one model took a bare user message for compaction input and replied with a summary line. eukhe prepends
Turn: the new message follows. Answer it as Eukhe, with your tools; do not summarize it.; prompt opening names both marks (memory/prompts.rsINTRO,TURN_TASK). Model-only; chat logs the user's words alone. Subagent's first message: no mark (task follows view).