Context and SQLite Store¶
tether.context¶
State Plane: ContextAssembler, Ephemeral Observation Folding, and Token Estimator.
ContextAssembler
¶
Assembles KV-cache-friendly prompts with deterministic observation folding.
calibrate(estimated, actual)
¶
Learn how far the 4-chars/token estimate undercounts for this model (never below 1).
build_system_prompt(toolbox_index, skill_instructions=None)
¶
Build the immutable system prompt prefix once per session.
effective_fold_after(turns, observations)
¶
K, or in auto mode K lengthened by the unedited re-reads (bounded by AUTO_FOLD_MAX_K).
assemble(*, system_prompt, goal, turns, observations, pins, current_step)
¶
Assemble the message array while preserving the static prefix and folding old outputs.
estimate_tokens(text)
¶
Fast deterministic token approximation (~4 chars per token).
estimate_messages_tokens(messages, tools=None)
¶
Estimate total tokens across messages and tool schema payload.
elide_active_observation(obs_id, raw_output, max_chars=2400, mem_enabled=True)
¶
Slice oversized active observations into Head + Tail in L1.
after_restart(turns)
¶
The turns from the latest restart message on (all of them when there was none).
modified_files(root, since)
¶
Up to 3 text files under root written after since, newest first (hidden dirs skipped).
describe_attempts(files, changes, reads, check_cmd, runs)
¶
What was tried, as counts: no command text from the failed steps is repeated.
restart_message(*, step, check_cmd, failing_output, files, tried, handback=0, fresh=None, failing=None)
¶
The one message a restarted history starts from, or (handback = restarts used) the
summary handed back to the caller; at most RESTART_MAX_CHARS. fresh names the files put
back as they started (a fresh attempt): the failing output, which quotes the failed code, is
then left out and failing (test names) is listed instead.
reread_count(turns, observations, root='')
¶
Number of executed reads that repeat an unedited earlier read of the same path+range.
live_reads(turns, observations, root='')
¶
Obs ids of the latest unedited read per path+range (max MAX_LIVE_READS); never folded.
Folding a still-valid read makes small models re-read it forever. A path stops being live
on edit write|replace or on any non-read bash command that mentions it (sed -i, tee, ...).
live_read_obs(cmd, turns, observations, root='')
¶
Obs id of the live read that cmd repeats, else None (edited, folded or never read).
A read is "live" if the file hasn't been edited since and its output is still in the prompt. Only the most recent MAX_LIVE_READS live reads are considered.
tombstone(obs, mem_enabled=True)
¶
One-line stand-in for a folded observation; recall hint only when mem exists.
tether.store¶
SQLite WAL state persistence and FTS5 BM25 RAG chunk store for Tether.
SQLiteStore
¶
ACID SQLite WAL state and FTS5 BM25 RAG store.
close()
¶
Close the current thread's cached SQLite connection.
list_nudges(session_id=None, model=None, limit=20)
¶
Most recent assistant replies the harness could not parse (newest first).
index_text(session_id, source_type, source_id, text, max_chunk_chars=600)
¶
Chunk and index text into the SQLite FTS5 chunks_fts table.
search_chunks(query, session_ids, source_types=None, source_id=None, top_k=3)
¶
Run a BM25-ranked FTS5 query across the specified session(s).
count_chunks(session_ids)
¶
Count indexed FTS chunks for the given session(s) (0 when none given).
get_project_session_ids()
¶
Return GLOBAL_SESSION_ID plus all COMPLETED session IDs for cross-session RAG.
search_project_chunks(query, *, source_types=None, source_id=None, top_k=3)
¶
Run a BM25 FTS5 query across project pins (__project__) and COMPLETED sessions.
get_session_digest(session_id)
¶
Build a compact ~50-token digest card for a session (avoids flooding parent context).
extract_friction_trace(session_id)
¶
Deterministic failed -> correction/fix lines from a session's turns (for retro).
get_stats(*, model=None, skill=None, limit=100)
¶
Aggregate per-model telemetry, token cost, and ACI friction offenders.
format_stats_report(*, model=None, skill=None, limit=100)
¶
Render the ultra-compact tether stats engineering scorecard and top offenders.
canonical_tool_command(raw_cmd)
¶
Normalize a raw tool command into its canonical <tool> or <tool> <subcommand> key.
chunk_text(text, max_chars=600)
¶
Split markdown or plain text into ~150-token chunks along headings and paragraphs.
sandbox_rule(output)
¶
The rule named by a sandbox refusal, cut to 80 chars; (unknown) if unparseable.
worker_log_path(db_path, session_id)
¶
Where a headless worker's stdout and stderr are kept (<db dir>/workers/<id>.log).
Structured history replay¶
When the provider sets structured_history = True (Ollama, OpenAI-compatible, Anthropic), ContextAssembler(structured=True) replays each past action as an assistant tool_calls message (act with cmd and a bounded stdin) followed by a tool message holding the (folded) observation, instead of act('...') text and Observation: user text that small models imitated into real arguments (#38). stdin is replayed as a real prefix (400 chars, then …), never a <N chars> placeholder that could be copied literally. max_context_tokens is a hard target: the assembler first folds live observations oldest-first (the newest stays), then drops the oldest call+result groups, then shrinks the newest observation, and scales the target by the real/estimated prompt-token ratio the provider reports (1.0 to 1.5). If goal, system prompt and the latest step alone do not fit, it prints one stderr warning (#81). A call and its result are one unit when trimming; an action with no result yet, and tool turns with no preceding action (pipeline digests), fall back to the textual form. The system prompt and tools array are unchanged (Rule 1).
Observation folding and small models¶
Raw tool outputs stay in the prompt while there is room. While the history fits in half of max_context_tokens (the target) nothing is folded by age. Above it, by default (auto mode, #205) outputs older than \(K=2\) turns fold into a one-line tombstone (see Built-in Tools) oldest-first, only until the history is back under the target, so crossing it by a little folds a little. Each unedited re-read of the same path and range lengthens \(K\) by one for the session, up to 8 (AUTO_FOLD_MAX_K). An explicit --fold-after N (or fold_after in providers.yaml or MCP) keeps the fixed rule: above the target every output older than \(N\) turns folds. The rule is deterministic: no model call and no static prompt tokens. For small models this has consequences (#27), seen in the course-builder example:
- Fix (#69). Folding exception: the latest read of each of the last 8 files (
edit view <p>,bash cat/head/tail/sed -n <p>; paths normalized against the workspace) is not folded until<p>is edited (edit write|replace, or any other bash command naming it:sed -i,tee, redirects,python -c) or re-read. The head+tail elision still applies (an observation may use up to a third ofmax_context_tokens, at least 2,400 chars), and the hard context budget remains #81. Themem recallhint is shown only whenmemis enabled (else "re-run the command"). (ContextAssembler(mem_enabled=..., workspace_root=...),MAX_LIVE_READS). - Re-viewing files loops, then invents data. The model runs
edit view, the output folds (two turns later once the context is more than half full), it views again, and eventually writes from memory. - What worked: one compact joined view, used right before the write, that ends with the exact next call. The
course-source <module>tool returns a module's plan and chosen videos in one output and finishes withNOW WRITE THIS MODULE: act("edit write part-N.md", ...), so the data is still in the prompt when the write happens. - Write long outputs in small pieces. One big write dropped a whole section. The writer skill writes one
part-N.mdper module withedit write, then acourse-buildtool concatenates them intocourse.md.
Restart after repeated failures, then hand back¶
For a model that is stuck, the history itself is the problem: the failed commands and their output pull it toward the same step again. Stuck means either of two things: the same check command has failed 3 times with an edit between each run, or the stall guard ended its warnings (repeated command, a cycle of commands, or one file re-read over shifting ranges) and the one [WRAP UP] turn did not help. Either way the harness adds one restart turn and continues from that turn alone. The message holds the files as they are now (up to 3, capped), the last failing output (trimmed) and short counts of what was tried. Failed commands are not replayed, so the model sees a state to fix rather than a transcript to copy. A session restarts at most twice. If it is stuck again after the second restart, the harness stops spending steps and hands the task back: the session ends WAITING_INPUT (exit code 2) with the same summary, headed [STUCK ...], as its result. The caller, a person or a parent agent, can refine the request and send it back. Everything before the restart turn stays in SQLite, and the restart costs no static prompt tokens and no extra model call.
Sources: Gumaan, Feedback That Backfires (arXiv:2608.23651), Kim et al., Scaling Test-Time Compute for Agentic Coding (arXiv:2604.16529) and Yang, Why Retrying Fails (arXiv:2605.08563).
Example of the message (trimmed):
[RESTART at step 14. Earlier steps are not shown; the files are as you left them.]
Your files now:
calc.py:
def add(a, b):
return a - b
Last result of `pytest -q`:
FAILED test_calc.py::test_add - assert -1 == 5
What was tried so far:
- calc.py changed 3 times
- `pytest -q` run 3 times with a failing result each time
Re-read the failing output above before you change anything.
The hand-back has the same body under the header [STUCK at step 31 after 2 restarts. Handing back to you.], and ends with "The work is not progressing. Refine the request or give guidance, then send it again."
When the restart comes from a failing check, it is a fresh start: the files the model changed go back to how they were when the session started (files it created stay), the message lists the tests that kept failing instead of the failed output, and asks for a different approach. A run that passes all but at most 3 tests (and more than twice as many as fail) is a near pass and is kept: when 3 later runs do not beat it, its files are put back in place with one line, and there is no restart, so the history stays:
[RESTORED] Your last 3 changes did not beat your best version, so its files are back. It failed only: test_meteredfile_iteration. Fix those from there.
After the first test run, edits are followed by an automatic test run ([auto-test]), and those runs count toward the 3 like the model's own. See "The Repair Loop Around Tests" in Architecture.