Skip to content

v0.57.0

Choose a tag to compare

@github-actions github-actions released this 21 Sep 23:00
· 6945 commits to main since this release

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>/index and <dir>/tui as 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 from setup, 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 an edit renders 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 $TMPDIR before 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 .gitignore files continuously (worker-build writes a one-byte * into build/.gitignore on 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 -shm or -wal through 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 restore honours path. 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 +2 starts at line 2 instead of returning the last two lines. The offset form was parsed as a count.
  • find rewrites only when the rewritten form answers the same question: a bare -name search still lists matching directories, and gitignored targets are not dropped.
  • grep declines 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.
  • includeTests is accepted in the same forms across every search surface.
  • aft_conflicts reports the files it could not scan instead of omitting them from a clean result.
  • aft_inspect refuses 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 setup and doctor configure a machine that has only OpenCode 2 installed. OpenCode now ships V2 under the plain opencode name, with a small opencode2 shim 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 yielded NaN. 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 --fix on 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 run doctor --fix for 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. doctor warned that plugins was unsupported, doctor --fix deleted it, and a correctly configured V2 host reported plugin registered: no — so repairing a working machine silently unregistered AFT. V1's plugin key is still used for V1 hosts, and a third-party entry under either key is left where its owner put it.
  • aft index, aft --version and doctor --profile find the binary doctor --fix installed. The resolver searched the download cache only when handed an explicit version, which those commands never pass, so on a machine with no PATH entry and no platform package the download succeeded and the next command reported no binary — naming doctor as the remedy the reader had just run. The not-found message now lists every location searched and why each was empty.
  • OpenCode 2: a bash call that omitted the optional description argument had its result discarded, and the model was handed Tool result missing while 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_outline on a directory counts the files it cannot parse instead of naming each one. A walk meets every .gitignore, LICENSE and 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.
  • edit tells 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 that symbol and content are only meaningful together, and the rejection names the missing or null property and both complete shapes. (#323)
  • Hashline PUT 2: INLINE reports 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_ms accepts up to 30 s. (#310, #320)
  • Windows: the ONNX runtime download extracts through %SystemRoot%\System32\tar.exe by absolute path (an MSYS2 tar on PATH read C:\ as a remote host), and doctor --fix downloads before it deletes the existing runtime. (#321)
  • PTY bash tasks resolve their shell through the same resolver as pipe tasks. $SHELL steered 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: true task 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_kill on a completed task signals the process group. (#311)
  • Concurrent aft.db openers 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_conflicts on a file path returns the file's conflicts instead of ENOTDIR.
  • Governed gh calls: read-only verbs (issue view, pr checks, run download, search, …) delegate to the caller's gh instead of refusing as unclassified; author-scoped issue edit is 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_unavailable with the retry deadline instead of building; a project in its refresh quiet window reports stale with the next refresh time instead of building. 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, so bun install or the first cargo build in 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 .gitignore writes no longer restarts the index load on every write.
  • A same-root aft_inspect shares 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 linked design-approved issue is converted to draft on open, and approving the issue marks it ready. The rule and the trivial bypass are described in CONTRIBUTING.md.

Configuration

  • semantic.query_timeout_ms ceiling: 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; health names 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 run may 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's dispose is 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)