Repository navigation
v0.57.0
AFT 0.57.0 makes OpenCode 2 a supported host, fixes a crash that could take down the daemon for every session on a machine, and two stalls that made every tool time out — a temp-directory scan that pinned the executor, and a scheduler that stopped refreshing one project while serving all the others. It also corrects eleven defects found by an external audit, stops three classes of runaway background work (a build tool rewriting one .gitignore byte no longer rebuilds the corpus), adds per-period disk-write attribution so the next write storm names its source, and moves the contributor gate to a fleet-shared workflow.
Highlights
-
OpenCode 2 is supported. Installed from a directory — the shape most people use for a local checkout — the plugin was never loaded at all: OpenCode 2 resolves
<dir>/server,<dir>/indexand<dir>/tuias literal paths rather than through the package's exports map, found nothing, and skipped it in silence with no log line. With entry points at those names the plugin loads, and the TUI sidebar registers from a mounted render rather than fromsetup, where the keymap primitive it called had no provider and took the whole TUI feature down with it. Permission-gated tools work: AFT reads the host's own configured rules, so an allow proceeds and a deny refuses naming the rule, and a rule that says ask raises a real prompt in OpenCode's own dock — the same engine as the built-in tools, so "always" answers persist and aneditrenders its diff. Approving one used to hang the call indefinitely, because the reply was announced before AFT was listening and OpenCode keeps no backlog of events. A refused call also reports as a failure now instead of a success carrying an error payload. -
Route binds no longer stall behind a temp-directory scan. The transient search-cache sweep listed the entire shared
$TMPDIRbefore applying its own 200-entry cap; with 290,000 entries from other tools' test scratch, one listing took ~870 s, six of eight executor workers sat inside it, and every new session on the machine was refused with "did not answer route.bind within 12s" while established sessions kept working. AFT's scratch now lives in its own subdirectory so the sweep is proportional to AFT's own files, the cap and deadline apply at the iterator, and the log reports what was examined, not only what was removed. -
A project the scheduler forgot. Tier-2 refreshes (dead code, complexity, duplicates) stopped dispatching for the most-used project on a machine while every other project refreshed normally; the health report kept publishing a deadline nothing honoured. The scheduler's only tick ran inside the watcher drain, the drain ran only when there was watcher work, and the tick recorded changes before checking its own quiet window — so the quiet window it waited for was exactly the window with no tick in it. A quiet project is now pumped by the maintenance tick, a limiter refusal backs off instead of retrying four times a second, and the daemon logs once, at info, when a published deadline passes without dispatch and why.
-
An ignore-file rewrite is no longer a corpus rebuild. Build tools rewrite nested
.gitignorefiles continuously (worker-buildwrites a one-byte*intobuild/.gitignoreon every run); each rewrite was read as a rule change and rebuilt the search index — 28 builds in fourteen minutes, 27 GiB/h of disk writes. An ignore file that is itself ignored is not a rule change; byte-identical rewrites trigger nothing; a change that only adds exclusions retires the newly-excluded paths instead of re-walking; a rebuild that is still needed is scoped to the changed file's subtree. -
Disk-write attribution.
aft profile --writes --since 2h(and--live 60) shows physical bytes written per subsystem and project over any window; the health report names the top three writers of the last ten minutes. This is the instrument that found the ignore-rewrite loop above. Bytes written by a checkpoint that runs as the last connection closes are reported as an unattributed remainder and named as such: measuring them requires reopening a live database's file set, which is the crash above. -
A crash that could end the daemon. Reading a SQLite database's
-shmor-walthrough a second file descriptor released every advisory lock this process held on that file — POSIX drops them all when any descriptor to the file is closed. Another process opening the same database then won the lock, reset the shared-memory file to three bytes as SQLite is entitled to do, and the next read through a mapping this process still held faulted. It cost one production daemon and a week of aborted CI shards. Nine places that opened a live database's file set no longer do: write accounting takes its frame counts from SQLite's own commit hook and checkpoint results, view durability stays inside SQLite, and copying a database that any connection still holds open is refused rather than attempted. The fault is reproduced by a two-process test that faults on demand and passes only when the lock survives.
Fixes
aft_safety restorehonourspath. A scoped restore validated the path, then restored every file in the checkpoint — a request to revert one file silently reverted the rest.- Literal search finds escaped patterns. A literal query containing backslash escapes (
foo\.bar,a\\b,split\("x"\)) was decomposed as a regular expression to pick trigram candidates, so files containing the exact text were never examined. tail -n +2starts at line 2 instead of returning the last two lines. The offset form was parsed as a count.findrewrites only when the rewritten form answers the same question: a bare-namesearch still lists matching directories, and gitignored targets are not dropped.grepdeclines the rewrite when basic and extended regular expressions disagree on a pattern, instead of answering with the other dialect's meaning.- A lone carriage return is line content, not a line ending, in both the edit path and the callgraph line index; a file using CR internally no longer shifts every offset after it.
includeTestsis accepted in the same forms across every search surface.aft_conflictsreports the files it could not scan instead of omitting them from a clean result.aft_inspectrefuses a scope it cannot resolve. A space-separated string was accepted as one path, matched nothing, and returned a clean bill of health over zero files; the reply now names the unresolvable path, distinguishes it from a path outside the project, and reports the analyzed-file count behind every number.npx @cortexkit/aft setupanddoctorconfigure a machine that has only OpenCode 2 installed. OpenCode now ships V2 under the plainopencodename, with a smallopencode2shim beside it, so the two names are different files and neither carries npm metadata; the only remaining signal was the version, and the probe passed the host's raw output (opencode v2.0.12) to an integer parse that yieldedNaN. Every such machine was reported as having both generations installed, and configuration was refused. Generation is now read from the version the executable reports, so the name it was invoked under no longer decides it.doctor --fixon an ambiguous host applies the fixes that have nothing to do with which generation is installed. The guard's own message said it was refusing configuration writes, but it returned before any fix ran — so a machine told to rundoctor --fixfor a missing ONNX Runtime ran it and got "no changes made". It now withholds only the plugin registration, runs the rest, and reports both sets.- The OpenCode 2 plugin registration is written under
plugins, the key that host reads.doctorwarned thatpluginswas unsupported,doctor --fixdeleted it, and a correctly configured V2 host reportedplugin registered: no— so repairing a working machine silently unregistered AFT. V1'spluginkey is still used for V1 hosts, and a third-party entry under either key is left where its owner put it. aft index,aft --versionanddoctor --profilefind the binarydoctor --fixinstalled. The resolver searched the download cache only when handed an explicit version, which those commands never pass, so on a machine with noPATHentry and no platform package the download succeeded and the next command reported no binary — namingdoctoras the remedy the reader had just run. The not-found message now lists every location searched and why each was empty.- OpenCode 2: a
bashcall that omitted the optionaldescriptionargument had its result discarded, and the model was handedTool result missingwhile the command had in fact run. The host's metadata schema rejects a key that is present with an undefined value, so the whole record was dropped; undefined-valued keys are now omitted rather than sent. Four further defects in the bash tool surface were invisible behind it and are fixed with it. - The OpenCode 2 sidebar renders the same panel as OpenCode 1 — project, session, both indexes, code health, compression and disk — instead of four lines, and reads the host's theme so it is legible in light mode. A badge whose background resolved to a transparent token is painted from the theme's own accent ramp rather than left unfilled.
aft_outlineon a directory counts the files it cannot parse instead of naming each one. A walk meets every.gitignore,LICENSEand lockfile in the tree, and listing them spent the reader's context on an answer they had not asked for; a request that names files still reports each one, and genuine parse failures are still named in both modes.edittells the model what is missing. The advertised schema is flat (hosts refuse root-level unions), so{path, symbol}passed validation and was rejected at runtime with a bare message; kimi-class models retried it nine times in a row. The tool text now states thatsymbolandcontentare only meaningful together, and the rejection names the missing or null property and both complete shapes. (#323)- Hashline
PUT 2: INLINEreports the body-row rule instead of "registers must begin with @". Trailing same-line text after a body-bearing address was parsed as a register reference. (#324) - Remote embedding backends that are slow are no longer classified as dead and re-collected from scratch. One 25 s deadline covered a whole 64-chunk batch and was retried three times unchanged, so a backend at 2 s per item could never finish; the deadline now scales with batch size and measured latency, a timeout halves the batch, and recovery re-embeds the failed file set instead of re-walking the corpus.
semantic.query_timeout_msaccepts up to 30 s. (#310, #320) - Windows: the ONNX runtime download extracts through
%SystemRoot%\System32\tar.exeby absolute path (an MSYS2taronPATHreadC:\as a remote host), anddoctor --fixdownloads before it deletes the existing runtime. (#321) - PTY bash tasks resolve their shell through the same resolver as pipe tasks.
$SHELLsteered them to the operator's fish or custom shell, so agent commands written for bash failed in ways the agent could not see. - Background bash completions delivered mid-turn no longer detach a running
wait: truetask and append the older task's footer to the wrong reply; busy deferral is capped at 60 s. - A background task that completes with a long-running descendant still alive reports it;
bash_killon a completed task signals the process group. (#311) - Concurrent
aft.dbopeners no longer wedge on migrations: version steps are planned inside the transaction that runs them, and a database left between V9 and V10 heals itself. Opening a current database takes no write lock. (#312) - Windows startup no longer downloads the same LSP servers sixteen times: auto-install is coordinated across plugin instances and the archive check uses the same extractor as the install. (#314)
aft_conflictson a file path returns the file's conflicts instead of ENOTDIR.- Governed
ghcalls: read-only verbs (issue view,pr checks,run download,search, …) delegate to the caller'sghinstead of refusing as unclassified; author-scopedissue editis classified; a probe that times out after the request was sent reports "outcome unknown" (exit 87) rather than "not run"; the shim's probe budget is 2 s per stage with one retry. - Embedding-backend outages are reported as
backend_unavailablewith the retry deadline instead ofbuilding; a project in its refresh quiet window reportsstalewith the next refresh time instead ofbuilding. Both had been read by the health sentinel as stuck index builds. - Windows CI: nine test races and path-form assertions fixed; none of them changed product behaviour.
- The Bun RPC server no longer keeps the process alive. Its Node counterpart has always unreffed the listener, with a comment saying the RPC server must not hold the process open; the Bun path never did.
Changes
- File watcher exclusion slots are seeded by ecosystem (
target/,node_modules/,build/,dist/, …) including nested copies, and directories that do not exist yet are excluded before they are created, sobun installor the firstcargo buildin a fresh worktree no longer overflows the event stream. Overflow log lines name the active exclusions and the path prefixes that produced the events. - Ignore-rule refreshes coalesce behind a 5 s quiet window; a burst of
.gitignorewrites no longer restarts the index load on every write. - A same-root
aft_inspectshares one cold-build slot across its five categories instead of taking five. - Callgraph refreshes write only the rows that changed; a one-file refresh on a 480 MB store went from ~105 MB of physical writes to ~16 KB.
- The daemon log keeps the newest dead process's log across a supervisor restart (it was the only file large enough to satisfy the directory budget, so it was the one deleted), and reaps relic plugin-log rotations.
- Hashline line tags are xxHash32. The seed-zero digest diverged for inputs of 16 bytes or more (an xxHash64-style merge was applied after the lane fold), so every tag for a line of 16 bytes or more changes with this release. Tag snapshots live in process memory and do not outlive a restart, so no stored state is invalidated; the digest is now pinned by independent test vectors.
- The repeat breaker keys on tool input rather than output, so alternating two calls or appending a timestamp no longer defeats it.
- Contributor pull requests are gated by the fleet's shared design-gate workflow (
cortexkit/subconscious): a PR without a linkeddesign-approvedissue is converted to draft on open, and approving the issue marks it ready. The rule and thetrivialbypass are described inCONTRIBUTING.md.
Configuration
semantic.query_timeout_msceiling: 15000 → 30000.bash.linux_scope(Linux, default off): run background tasks in a transient systemd scope so their descendants are tracked.
Notes
- Content-addressed views (
views.enabled) remain opt-in; this release carries its write-volume and materialization work but no default change. - A Tier-2 code-health scan no longer starts while the same project's call-graph is still building its cold index. The two read the same store, and on a repository whose cold build runs past ninety seconds the scan was starting on top of it — costing roughly 90 MB of peak memory at the moment the machine was already busiest. The warm scan is held rather than dropped, and starts as soon as the build finishes;
healthnames the build it is waiting on. - The OpenCode 2 end-to-end matrix is now a required check on
main, so a change that breaks OpenCode 2 support cannot land. It covers every registered tool across seven trajectories on a real GA host, including V1/V2 behaviour parity. - On OpenCode 1,
opencode runmay not exit after a one-shot run in a git-tracked project. This is the host's, not AFT's: it reproduces with no plugin loaded at all, and the trigger is the git repository rather than anything a plugin does. AFT'sdisposeis called and returns normally where the host reaches it. - The search benchmark no longer indexes its own answer key. Its fixture and baseline files contain each fixture's query and its expected files, and they sat in the corpus being measured, so 40 of 41 fixtures retrieved an answer-key file in their top ten. The recorded baseline was therefore lower than the engine's real behaviour, which flattered every candidate measured against it; the baseline is re-recorded on one binary with the exclusion in place. The gate also runs on Windows now, where it previously died on its first request and, once past that, reported a clean zero on a healthy index — a shape indistinguishable from a total ranking regression. A report with every fixture unmatched is now refused rather than written. (#325, #326, reported by @Qiiks)