Release Notes¶
v0.3.0 (2026-11-09)¶
Breaking changes¶
- Without
bwrap, aworkspace-writesession with the kernel sandbox on is now refused before the first model call (exit 1). Migration: pass--allow-unsandboxed(SDKSandboxPolicy(allow_unsandboxed=True)/Harness(allow_unsandboxed=True), MCPallow_unsandboxed), or setTETHER_SANDBOX=containerin Docker. With the opt-out, one[sandbox] bwrap not available, running with command checks onlyline is printed on stderr (#124 item 2).
Changed¶
- Sandbox refusals now name the rule, what to do instead, and the flag that would permit it when one exists (#124 item 3). No change to which commands are allowed or refused.
- Observation folding is automatic by default: above half of
max_context_tokens, old outputs fold oldest-first only until the history is back under that target (no cliff), and each unedited re-read lengthens K by one (cap 8).--fold-after Nkeeps the fixed rule (#205).
Added¶
- Sandbox conformance suite: a table of must-run and must-refuse commands, run against the text layer and real bwrap. It proves items 1 and 9 of #124.
- Escalation: ask the caller once, then remember (#59, #124 item 4). A command whose prefix is in the policy's
escalatelist (defaultgit push|pull|fetch|clone|worktree|config,gh,docker) and not allowlisted pauses the session asWAITING_INPUT(exit 2) with[APPROVAL NEEDED] <command>and the JSON keyapproval.tether send <id> approve(or the newtether allow <id>, MCPtether_send) adds the prefix to.tether/allow.json, runs the command outside the jail and resumes; any other reply refuses it. Never offered for metacharacters, unsafe git options, read-only or--no-networksessions. - Allowlist of command prefixes that run outside the jail (#59, #124 item 4):
tether run --allow(repeatable),allowin.tether/allow.json, and the MCPtether_runargumentallow. Whole-token prefixes, no shell, never in read-only or--no-networksessions. - Sandbox refusals are recorded as
sandbox_violationevents with their rule, andtether statsshows the top rules (#124). - Opt-in resource limits for
bash(#124 item 6):tether run/pipeline --limit KEY=VALUE(cpu, mem, fsize, procs),SandboxPolicy(limits=)and the MCPtether_runargumentlimits; off by default, Linux only, a hit adds a[limit hit: ...]line. - Web domain allowlist (#124):
tether run --web-allow(andSandboxPolicy(web_allow=)) restricts thewebtool to listed domains; sub-agents and stages can only narrow it. tether pipeline --web-allowand the MCPtether_runargumentweb_allowset the same domain allowlist. A malformed value (spaces, a*outside a leading*., a URL) is a usage error or a ToolError, not a silent pass.- Docs: install with the network, work without it (
--setup,--no-network) (#124). tether statusshowssandbox: bwraporsandbox: command checks only.tether export <id>prints a session as a Markdown transcript (--out FILEto save it).- A
--verifygate whose program does not exist (checked for every command in a&&/||/;/|chain) now fails the session before the first model call, as a misconfigured gate (#82). tether doctorchecks your setup (Ollama reachable, model pulled, bwrap, providers.yaml, session DB) and says what to fix; exit 1 when a check fails.tether run --worktreeruns the session in its own git worktree on branchtether/<id>and commits the result there when it stops.--worktree-from <ref>sets the base ref and--setup <cmd>runs a shell command in the worktree before the model starts (outside the sandbox, not offered over MCP). See Run a session on its own branch.tether read <id> --thinkingprints the stored model thinking for each step. Thinking is kept for reading only and is never replayed to the model.tether replay SESSION_IDre-runs a session's goal in the current directory with its saved tools, skill, sandbox, output_schema and params, and prints the original and replay side by side (--model,--max-steps,--json). Verify gates are not stored, so a replay runs without them.tether pipeline --resume <id>re-runs a failed or waiting pipeline from its first unfinished stage and reuses the stages that completed (#35).--max-tokens N(CLI),Harness(max_tokens=)and the MCPtether_runargumentmax_tokensset a token budget (prompt + completion). When the budget is reached the session pauses as WAITING_INPUT (exit 2) andsendadds the same amount again as a grant. Sessions without a budget are unchanged.tether mcp:tether_runacceptsworkspace, an absolute directory inside the server's workspace, so a caller working in a git worktree can root the run there; anything outside is refused (#242).tether diff <session_id>shows what a session changed in its files, against the first bytes the session saw (--statfor line counts). Snapshots are recorded by the harness write-tracking loop in a newfile_snapshotstable; files created by the session show as added from empty.
Fixed¶
- A headless worker that dies before it starts no longer leaves its session PENDING: it is marked FAILED with
Worker died (pid N) before starting, and the result names the worker's log (<db dir>/workers/<id>.log), which now keeps the worker's stdout and stderr (#271). edit writeon an existing file with stdin made only of complete SEARCH/REPLACE blocks now applies them asedit replaceinstead of replacing the whole file (#267).- Replay and list columns no longer collide with long model ids or WAITING_INPUT, and
replayaccepts--worktree,--worktree-fromand--setup(#279). tether waitexits 6 and printstimed out; still RUNNING: ...on stderr when the timeout expires (#269).- Command provider timeouts and empty failures are retried twice before raising
ProviderError(#268). - Headless workers always import the installed tether, never a workspace's own tether/ package (#273).
- An empty model reply is retried. If the retry is empty too, the session stops instead of continuing with no output (#135).
tether run --worktreeno longer commits bytecode and tool caches (__pycache__,*.pyc,.pytest_cache,.ruff_cache,.mypy_cache,.tether/) (#278).tether diffshows writes bysed -i,perl -i,mv,cp,python -c, and names files changed by commands it could not track instead ofno file changes recorded(#280).- Masked secret files and
.git/confignow read as empty instead of "Permission denied", sogit status,git logandgrep -rwork inside the sandbox (#124 item 1).
v0.2.18 (2026-11-08)¶
Small fixes from the v0.2.17 release check and the paasio labs: shell-style file writes work when only edit is enabled, edit append adds to a file instead of replacing it, a foreground run prints its session id at once, and a cd into a container path that does not exist no longer costs a step.
Added¶
edit append <path>addsstdinto the end of a file (created if missing, with a newline added when the file does not end with one). Small models assumededit writeappends: gemma4:e4b wrote a plan module by module and kept only the last one. Theedithelp now sayswrite replaces the fileand stays within 59 tokens; withbashoff,echo t >> fruns asedit append f(#34).- A foreground
tether run(with or without--json) writessession ses_<id>to stderr as soon as the session is created, before the first model call. A caller that kills the run on its own timeout no longer needstether listto find the id forstatus,stoporsend; stdout is unchanged (#90).
Fixed¶
cd /testbed && python3 -m unittest ..., with a directory that does not exist, now runs the command after thecdin the workspace, with one line saying thecdwas skipped. Models assume a container path; qwen3.5:9b lost a step to it in 3 of the last 6 paasio runs. Acdinto a real directory runs as before (#79).tether statsoffenders: a leadingcd <dir> &&no longer makescdthe offending command (the command after it is counted), and a turn without a command shows as(no command)instead ofunknown(#79).- With
editenabled andbashnot, a plain shell file write or read now runs asedit:echo ok > hello.txt,printf 'a\nb' > f,cat > f << 'EOF' ... EOFbecomeedit write, andcat fbecomesedit view f. Any other shell command still answersunknown command, now with a pointer toedit view|write. In the v0.2.17 release check (--tools edit, "Create hello.txt containing ok"), qwen3.5:9b triedecho,printf,catandls10 times before writing the file at step 12 of 15. Pipes are not translated.
v0.2.17 (2026-11-07)¶
When tests fail, tether now works around them on its own time: it re-runs them after each edit, traces the smallest failing test, starts fresh when an approach keeps failing, and keeps a version that nearly passes instead of throwing it away. The north star (74% on Polyglot-Python with qwen3.5:9b) moves to v0.4.0. No full Polyglot run was made for this release; the numbers below are from targeted labs.
Added¶
- A repair loop around failing tests, all from the harness's own time (0 static prompt tokens, no extra model call). Once the model has run the tests, they run again after each edit and the result is appended (
[auto-test]), so no version is typed blind. When the model reads the tests of the file it is about to write, it is asked once to say in plain words what the hardest tests call and expect ([BEFORE WRITING]). When the same tests fail again after a change, or only a few still fail, a runtime trace of the smallest failing test shows the calls into your code with their arguments and results. When at most 3 tests still fail, one note points toedit replaceinstead of a whole-file rewrite. Onpaasiowith qwen3.5:9b (600 s cap) the mean score went from 0.63 over 3 runs to 0.94 over 5 runs, with 1 of 5 passing; the other 4 ended at the time cap with 22 to 24 of 25 tests passing (#173).
Changed¶
- The north star is now 74% (50 of 68) on Polyglot-Python with qwen3.5:9b by v0.4.0, instead of 80% by v0.3.0. 74% is the share of exercises the model has already solved at least once from scratch (25 of 34), so the target is reliability: each of them passing every run. The 9 exercises it never solved are out of scope for this target (#173).
- A restart after the same tests failed 3 times is now a fresh start: the files the model changed go back to how they started, and the restart message names the failing tests instead of quoting the failed code (Olausson et al., ICLR 2024). A version that already passes all but at most 3 tests is kept instead: when later edits do not beat it, its files come back in place with a
[RESTORED]line and the history stays (#173). - The number of failing tests is read from the runner's summary line (
FAILED (failures=1, errors=3)), so a test run cut by| headis no longer taken for a near pass (#173).
Fixed¶
edit write hello.txt ok, with the content typed after the path and nostdin, now writesokinstead of answeringError: `edit write` requires file content in `stdin`. qwen3.5:9b lost two steps this way creating a one-line file in the v0.2.16 smoke run. The text is taken as typed (spacing kept, one pair of surrounding quotes removed, literal\nunescaped);stdin, when given, still wins (#173).- In
tether list, a pipeline's own row now shows the model its stages run on instead of the harness default (ollama/qwen3.5:9beven when every stage setmodel: local/qwen3.5:9b). When stages use different models, the row lists them in stage order, joined with,. A stage with nomodel:still runs on the pipeline's default model. - A test run piped to
head(python3 -m unittest ... 2>&1 | head -100) now keeps its result. The runner prints its summary (Ran 26 tests/FAILED (failures=8)) last, soheadcut it, and the pipe also replaced the tests' exit code withhead's 0: the model could not tell how many tests failed, and tether's own notes on test runs (failing test source, regressions, green-test finish) did not fire. Now tether runs the test command itself, keeps the first N lines and adds[... K lines cut by head; the end of the output:]with the last 6 lines and the real[exit N]. In the v0.2.15 Polyglot run on qwen3.5:9b, 22 of 39 failing runs had at least one test result cut this way, against 7 of 47 passing runs. On 4 of those exercises plus 2 passing controls (32k, one run each), e ... [truncated 48,068 chars by sandbox cap] ... - Multiplexer: small models that send
edit write/edit replacestdin or thedonedeliverable as one line with literal\n(and\t) escapes now get them unescaped deterministically (only when the stdin has no real newline).bashstdin and all other tools are never touched. (#26)
v0.1.1 (2026-10-18)¶
Fixes¶
- Bash: a command with no output now returns
[exit 0: no output] Command succeeded.instead of a bare[exit 0], which 9B models misread as failure and repeated until the loop breaker fired. - Docs: new tutorial page for gates,
done --ask, project memory, pipelines,statsandretro; added the documented-but-missingtether --version.
v0.1.0 (2026-10-17)¶
Milestone v0.1: Deterministic Control Plane¶
- Pre-
doneVerification Gates:Harness(verify=[...]),--verify, or Skillverify:run inside the sessionSandboxPolicybeforedonecan complete a session (0 static prompt tokens until a gate fails). - Interruptible
WAITING_INPUT&done --ask: sessions can pause for human/parent input and resume viatether send. - Hierarchical Project Memory:
mem pin --project/mem search --projectwith cross-session BM25 RAG. - Telemetry & Failure-Mode Inspector (
tether stats): per-model pass rate, token cost, step latency and ACI friction offenders. - Deterministic Multi-Stage Pipelines (
tether pipeline): isolated stage sessions, heterogeneous model routing, monotonic sandbox attenuation, ~50-token stage handoffs. - Post-Mortem Distillation (
tether retro):tether retro <session_id> [--apply]extracts a session's friction trace (inbox corrections, loop stalls, syntax/tool errors, gate failures) and distills at most 2 deduplicated rules (<= 30 tokens each) intoSKILL.mdunder## Learned Rules, or project memory when no skill was used. Runs offline, never mutating a live prompt.
Fixes found during release validation¶
- Sandbox:
bwrapnow really unshares PID/IPC namespaces (docs claimed it); underbwrap,.env*,.tether/,.git/configand.git/hooksare now masked at the kernel level. Previously only a command-string regex guarded them, socat .e*orcat $(echo .en)vread secrets andecho >> .git/con*modified git config.SSH_AUTH_SOCK/GPG_AGENT_INFOare scrubbed and/run/useris hidden. tether stats: pipeline root sessions are excluded; they invented a run for the default model and double-counted stage tokens.- Multiplexer: an unknown first token (e.g. a raw
echo hi > f) now hintsbash <cmd>when thebashtool is enabled; small models previously looped on the bare error. - CLI:
tether pipeline(missing file, invalid YAML, stage withoutgoal) andtether send <unknown id>printError: ...and exit 1 instead of a traceback.
v0.0.3 (2026-10-17)¶
Architectural & Performance Upgrades¶
- Head + Tail L1 Active Observation Elision (
tether/context.py): Oversized active observations (> 2,400 chars) are deterministically elided in L1 working memory to their first 900 chars (headers/command summary) + last 900 chars (stack trace/exit code) with a pointer tomem recall/mem search, preserving full outputs in SQLite FTS5. - Deterministic Loop-Stall Breaker (
tether/harness.py): Consecutive identical(cmd, stdin)calls from small 8B/9B models are intercepted before tool execution and answered with a deterministic[LOOP DETECTED]recovery nudge. - 100% KV-Cache Prefix Immutability Across Pins (
tether/context.py,tether/providers/anthropic.py):[PINNED MEMORY]is attached to the tail message after window trimming so earlier turn messages remain 100% byte-identical when pins change; added Anthropic ephemeralcache_controlbreakpoints. - Persistent Connection Pooling & Thread-Local SQLite WAL (
tether/providers/*,tether/store.py): Reuses persistenthttpx.AsyncClientinstances across turns and caches thread-local SQLite WAL connections withPRAGMA temp_store=MEMORY;and 128 MBmmap_size.
Packaging, Documentation & CI¶
- PyPI Trusted Publishing (
tether-sdk): Published on PyPI astether-sdk(pip install tether-sdk/uv add tether-sdk) with automated OIDC delivery (.github/workflows/delivery.yml). - Flattened Package Layout: Moved
tether/to the repository root (removingsrc/). - MkDocs Documentation Site: Added full documentation, tutorials, literature review, and
mkdocstringsAPI reference athttps://igormcsouza.github.io/tether/(.github/workflows/docs.yml). - Modular Test Suite & 100% Coverage CI Gate: Split unit tests into 20 per-module test files under
tests/and added.github/workflows/ci.ymlenforcingruff,mypy,mkdocs build --strict, andpytest --cov=tether --cov-fail-under=100.
v0.0.2 (2026-10-17)¶
SDK Provider Extensibility & Literature Grounding¶
- Custom SDK Provider Registry (
register_provider): Addedregister_provider(prefix, factory)intether.providersso SDK consumers can route<prefix>/<model>identifiers through custom gateways, local inference engines, or test transports. - Literature Grounding: Documented architectural alignment with Maia et al. (2026), Wang et al. (CodeAct, ICML 2024), Yang et al. (SWE-agent, NeurIPS 2024), and Packer et al. (MemGPT, 2023).
v0.0.1 (2026-10-16)¶
Initial Release¶
- Single
act(cmd, stdin)action multiplexer with< 150tokens of base framing overhead. @toolPython function introspection with JIT--helpdisclosure (<= 60tokens).- SQLite WAL session store + FTS5 BM25 RAG (
chunks_fts) and ephemeral \(K\)-turn observation folding. - Detached headless worker execution (
tether run --headless,wait,send,query,read,list). - Always-async multi-agent orchestration (
agent spawn,wait,query,read,send). - Deterministic
SandboxPolicywith workspace path confinement, secret environment scrubbing, and Linuxbwrapisolation.