# Changelog All notable changes to Noeta are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The 0.x releases (packages `noeta-runtime` and `noeta-sdk`) were a different codebase; their history is not carried in this repository. Their versions are listed on PyPI. ## [1.2.3] - 2026-11-10 Noeta stays quick however long the conversation. New messages no longer wait on the skills directory, tasks start in bypass-permissions mode, the TUI and the web app keep up with long sessions, a rewound conversation shows whole, and both fold tool calls as Claude Code does. ### Changed - **`Store.read` takes a keyword `limit`** (the head of a log; `ClientAPI.events` and `GET /tasks/{id}/events?limit=` pass it through). A third-party store adapter must accept it; `noeta.testing.store_contract` checks it. - **Tasks start in `bypassPermissions` by default**, in the TUI, `noeta -p`, ACP, sub-agents, the web app and served tasks; deny rules, ask rules and protected paths still hold in it. `noeta serve` and `noeta worker` allow it unless started with the new `--disable-bypass-permissions`; `--allow-bypass-permissions` is still accepted and does nothing, so an installed `noeta service` keeps starting. A settings file's `permissions.disableBypassPermissionsMode: "disable"` still switches it off; where it is off, tasks start in `auto` as before. - Long diffs fold after 12 lines ("Show N more lines") and show line numbers. - Tool calls are drawn as lines without boxes; dark mode has its own error and status colors. - The rewind dialog says what each choice does (Both / Conversation / Code). - ↑ in an empty message box brings back your earlier prompts. - Runs of tool calls fold into one line in Claude Code's words ("Searched for 2 patterns, read 3 files, ran 1 shell command"), opened on a click; a failed call still shows on its own, edits of one file in a row and agents started together are merged, and task-list calls are left out (the task panel shows the list). - Projects that share a name show the parent folders that tell them apart. - **`Ctrl+O` shows the detailed transcript** in the transcript's place, as Claude Code's transcript mode does, instead of unfolding every call in place; the transcript stays folded. - **Tool calls fold as Claude Code's do**: a single call shows as itself and its line turns into the count, in place, when a second joins; short remarks between calls, answered approvals and the task list no longer break it; a failed call pops out with its first error lines; shell commands that only read, search or list count as that, and commits, pushes and pull requests are named; reads count files, MCP calls name their server; a running read shows its pattern as a hint; consecutive edits to one file are one line (`Edited calc.py ×3 (+12 −4)`), and agents started together are one line with a line each. ### Fixed - **Every new message was slow with a large skills directory**: the skills were indexed again for each message, and indexing compared every file with every skill (57 skills over 2,174 files took 1.2 s). The index is built in one pass (43 ms) and only again when a skill file changes; a message after the first now answers in about 50 ms instead of 1.4 s against a zero-latency model. - **Each model call first ran `find | cat | cksum` over every skills directory** to notice a skill added mid-turn (about 15 ms a step). On this machine the look is now made by `stat`; a sandbox or remote exec env still runs the command through the env. - **A `Bash` call waited at least 50 ms after its command ended**, polling for its output to be written: `echo hi` took about 60 ms. The shell now wakes as soon as the output is written (a few ms), still giving a child left running with `&` at most a second. - **Typing lagged while a long session streamed**: every streamed chunk re-rendered the whole page (about 1,200 components in a 700-event session) and re-parsed the whole answer. Only the streaming message redraws now, once per frame, parsing just the paragraph being written; typing answers in ~14 ms instead of 120–230 ms. - **Opening a long session was slow and downloaded its log twice**: it now draws the latest messages first and the earlier ones while the page is idle, and reads the log once (a 700-event session shows its end in ~170 ms instead of ~490 ms). - **Scrolling up while the model wrote snapped back to the bottom**: the view now stays where you scroll it, with a "Jump to latest" button to return. - **Errors showed Python exception text** (`RuntimeError('…')`) and tool errors their `` tags: each is one plain line now, the raw text under "Details". - **Esc did not close the rewind and plugins dialogs.** - **With a side panel open, the message box did not line up with the conversation**, and the session's folder squeezed its title in the header. - **The message box did not grow for wrapped lines** on a narrow screen. - The session list and approval cards are no longer polled while the page is hidden. - A new web session on a server started with `--disable-bypass-permissions` starts in `auto` instead of failing; any other refusal is shown, not retried in another mode. - **`noeta serve` let requests start tasks in `bypassPermissions` when only a settings file disabled it** (`permissions.disableBypassPermissionsMode: "disable"`): the plugin then ran them in `auto` while the web app and the headless protocol's init message said `bypassPermissions`. The server now refuses it as with `--disable-bypass-permissions`, and the init message names the mode the settings start a task in (their `defaultMode` too). - **The TUI got slower with every message**: every message stayed laid out, the spinner and the footer laid out the whole screen several times a second, and each streamed fragment was parsed and laid out on its own. At 500 messages the idle spinner held the CPU at 68%, a key took up to 650 ms to show, a resumed task took 10–18 s to appear and a 4000-character answer took two minutes to stream. Only the transcript's end is laid out now (older messages come back as you scroll up), the spinner and footer are redrawn without a layout, a streamed reply is shown once a frame, and a finished reply is drawn as a few pieces of text (tables one piece each) instead of Textual's widget per block and per table cell; while a reply streams only the block being written stays live. - **Scrolled up, the transcript was pulled back to the end** by every record; it now stays where you are and follows again once you scroll back down. `PageUp` / `PageDown` scroll it while the prompt has the keys. - **`Esc` took a moment to stop the turn on screen**: it now stops the spinner and says Interrupted at once. - **Typing after `@` and `Ctrl+V` froze the screen** while the directory was walked or the clipboard program answered; both run off the screen's loop, and in a git repository the `@` list is git's (ignored files left out). - **A rewound conversation lost what came before the rewind point** on screen, and on every later resume: the conversation it continues is now shown first. A rewind also left an empty task behind when nothing was sent after it; its task is now made with the first message. - **`--resume` opened the newest task before listing**, and opening it reordered the list; it now lists first and opens only the task picked. The list is this directory's conversations (a last choice lists every directory's), every one of them, a rewound one as one line (the conversation it continues is listed again once you go on with it). It reads only the start of each conversation's log: 200 conversations list in 0.1 s instead of 0.6 s, and over `--server` without fetching every whole log. ## [1.2.2] - 2026-11-10 Images reach gateway models again, switching between auto and bypass-permissions mode no longer sends contradicting notes, and the TUI shows link addresses. ### Fixed - **Images never reached a model listed in a provider table or models file without `supports_vision`**: such a row counted as text-only, so every image was replaced by "[image omitted: model is not vision-capable]". A row that leaves the field out now sends images and lets the provider judge, as a model nobody listed already did; `supports_vision = false` still marks a text-only model. - **Switching from auto to bypass-permissions mode told the model two opposite things**: "auto mode has ended, go back to the dedicated tools", then "the shell may also do small jobs". Moving between the two modes now sends no note, and leaving bypass-permissions mode for another mode sends its own exit note. - **The TUI hid link addresses**: a link showed only its text, which a terminal reached over ssh cannot open. A link now reads `text (address)`, unless its text already is the address. ## [1.2.1] - 2026-11-10 `noeta update` installs the new version even when uv cached the old listing, and `noeta service` takes along the keys `config.toml` refers to. ### Fixed - **`noeta update` reinstalled the version already there** when uv had cached PyPI's listing from before the release: it now asks uv to refresh noeta's packages (`pip` runs without its cache), and says so when the index still serves the old version instead of reporting success. - **`noeta service install` left out the keys `config.toml` refers to** (`{env:NAME}`, for example a provider's `api_key`), so the service could not reach its model: those variables now go into its environment too, and one the shell does not set is named. ## [1.2.0] - 2026-11-09 One command runs the backend, `noeta serve`, and `noeta service` keeps it running as your user's service; a session without a project gets a scratch directory of its own. ### Removed - **`noeta web`** (and `noeta web status` / `stop`): run `noeta serve`, which prints the address to open, or install it as a service with `noeta service install`. - **`noeta serve --allow-root`**: a request may name any existing directory as a task's, project's or folder listing's `cwd`; only one that does not exist or is not a directory is refused (`400`, was `403` outside the server's directories). A token holder can run any command as the server's user, so the old check guarded nothing. - The warning that a non-loopback `noeta serve` without `--allow-host` answers any `Host` header: it no longer does (below). ### Added - **`noeta service install|uninstall|start|stop|restart|status|logs`**: `noeta serve` as a systemd user unit (Linux) or a launchd LaunchAgent (macOS), started at login and again after a crash. `install` takes `noeta serve`'s flags and copies the shell's `NOETA_*`, provider, proxy, `PATH` and `HOME` variables into an owner-only file; on Linux without lingering it says the service stops at logout. `noeta update` restarts an installed service after upgrading. - **Every `noeta serve` start prints the address to open with the token** (`http://127.0.0.1:8642/#token=…`; one line per name and address on a non-loopback bind). - **Scratch directories**: a task created over HTTP without a `cwd` runs in `~/.noeta/scratch///`. `GET /projects` never lists them as projects; it ends with one `Scratch` entry (`scratch: true`) counting their sessions, and every entry has a `scratch` field. - **Scratch sessions in the web and desktop apps**: "New session" with no project picked makes a session without a directory, which the server runs in a scratch folder of its own. The sidebar lists those sessions after the projects under **Scratch**, grouped by day ("Today", "Nov 9"), newest first; picking Scratch, or **Use a scratch folder instead** on the welcome screen, goes back to scratch for the next session. ### Changed - **`~/.noeta/server-token` holds the token itself** (owner-only) instead of its digest (ADR-034, superseding ADR-013); a digest-only file from an older noeta gets a new token on the next start. `noeta --server` and `noeta acp --server` on the same machine read it when `NOETA_SERVER_TOKEN` is unset. - **A non-loopback `noeta serve` answers this machine's own names**: its hostname, fully qualified name and addresses, besides the loopback names and `--allow-host` ones; any other `Host` is refused. - `GET /dirs` lists any directory; its `parent` is `null` only at `/`. - **The web app's folder picker goes anywhere**: any existing directory can be added as a project; a path that does not exist or is not a directory shows the server's reason. The hints about allowed folders are gone. - **The desktop app starts its server without `--allow-root`** and no longer restarts it for a folder outside your home directory: a folder picked anywhere is saved as a project at once. The `roots` an older version kept in `settings.json` are dropped on the next save. With no project saved, new sessions get scratch folders, as in the browser. ### Fixed - **A foreground command that leaves a child running returns sooner on macOS**: the up-to-one-second wait for its output started a `sleep` every 20 ms, which on macOS stretched the second to two or three; it now ticks every 50 ms. ## [1.1.3] - 2026-11-09 `noeta web` tells you how to use a folder it refuses, and the web app's browser tests stop reading the tester's own skills and settings. ### Fixed - **`noeta web` says how to use a folder it refuses**: a project or session outside its folders now tells you to run `noeta web stop` and start `noeta web` from that folder, not to pass `--allow-root`, which only `noeta serve` takes. - **The web app's browser tests no longer read the tester's own skills and settings**: the test server runs with its own `HOME`, so `~/.claude/skills` cannot crowd the slash-command menu. ## [1.1.2] - 2026-11-09 `noeta web` opens the web app with no build step, the web and desktop apps become a local workbench of projects and their sessions, `noeta update` upgrades in place, an unexpected error prints one line instead of a traceback, and each release carries desktop installers. ### Added - **`noeta web`**: the web app in your browser with no build step. It starts `noeta serve` on `127.0.0.1` in the background (port 8642, else a free one) with its own token, waits until it answers, prints the address and opens it; a second `noeta web` reuses the running server. `noeta web status` and `noeta web stop`; `--port`, `--no-open`. The server may run tasks in the directory it was started from and anywhere under your home directory. - **The wheel carries the built web app**, and `noeta serve` serves it when `--web` is not given. `make build` and the release build it first (`make web-bundle`, needs npm). - **`noeta update`**: checks PyPI and upgrades in place, keeping what noeta was installed with: a uv tool install is reinstalled with the extras, `--with` packages, Python and index its receipt names; a pip install gets `pip install -U` with the extras installed; a checkout is left alone (it prints the commands). A running `noeta web` server is restarted on the new version. - **Projects in the HTTP API**: `GET /projects` lists the saved directories and those of unarchived sessions, each with its count of sessions; `POST /projects` / `DELETE /projects` save and unsave one (checked as a new task's `cwd`); `GET /dirs?path=` lists a directory's subdirectories for a folder picker. - **Desktop installers on each GitHub Release**: macOS `.dmg` and Linux `.AppImage` / `.deb`, built by the release workflow and not signed (on macOS, right-click the app and choose Open the first time). ### Changed - **The web and desktop apps are a local workbench**, as Codex's desktop app: the left column lists projects (folders, by name with the path on hover), each with its sessions newest first, a status dot and how long ago each moved; New session at the top starts one in the selected project, a project's + starts one there, and the empty session names the project it will run in. Add project opens a folder picker over the server's folders in a browser (`GET /dirs`, saved with `POST /projects`; a folder the server may not use says why) and the system's picker in the desktop app; a project you added can be removed from its menu. The page remembers the last project; the desktop app opens on it. Search and the archived list work across all projects. Against a server without `/projects`, the folders of its sessions are listed. - **Desktop**: File > Open Project Folder is now File > Add Project… (still `⌘O`), and the chosen folder is saved as a project of the server. The install line it shows when `noeta` is missing is `uv tool install 'noeta[tui,server]'`. - **An unexpected error prints one line** (`noeta: unexpected error: : `) and the path of a crash report under `~/.noeta/logs/`, not a Python traceback; `--verbose` or `NOETA_DEBUG=1` prints the traceback too. Ctrl+C exits with code 130 without a traceback. The terminal UI no longer prints Textual's traceback, which showed every frame's local variables. ## [1.1.1] - 2026-11-09 The terminal UI copies text, reopens long tasks at once, folds runs of tool calls as Claude Code does, shows the effort and, through the new `project_status` plugin, the project and git branch. On macOS, background commands, deny rules over globs and servers on this machine work as on Linux. Reading a repository's git state can no longer start a program its config names through a partial clone's fetch. ### Added - **Copy from the terminal UI**: drag over any text in the transcript (replies, tool calls and their output) and it is copied when you let go; `Ctrl+C` copies the selection (and then neither clears the input nor counts toward quitting); `/copy [n]` copies the last reply, as Claude Code's. The text goes by OSC 52, plus tmux's buffer inside tmux and `pbcopy` / `wl-copy` / `xclip` / `xsel` when not over ssh. - **Tool calls in a row fold into one line**, as Claude Code's: `● Searched for 2 patterns, read 3 files, ran 2 shell commands (ctrl+o to expand)` (reads, searches, listings, web, shell commands, other tools, and the thinking between them); present tense while one runs, failures counted in red. A click opens the line or a call's folded output. Paths under the task's directory are shown relative to it. - **`project_status`**, a shipped plugin (on in `main`, switchable in `/plugins`): the project and git branch of the task's directory (`noeta · ⎇ main* ↑1`) as status items, on the line under the terminal UI's footer and in the web and desktop status lines. Git runs with the repository's programs (fsmonitor, filters, hooks) and every transport switched off, cached for two seconds. The footer no longer shows the directory. ### Fixed - **Reading a repository's git state can no longer run a program its config names through a fetch**: in a partial clone, a read that meets a missing object fetches it, through the repository's own `core.sshCommand`, an `ext::` URL it allows or a credential helper. The git commands noeta runs itself (the environment block's snapshot) now allow no transport and set `GIT_NO_LAZY_FETCH`. - **Reopening a task in the terminal UI is fast**: its history is rendered first and shown at once (a 424-event task: 1.3 s, was 12.5 s); each record shown live costs a fifth of what it did. A reopened turn's `Worked for` comes from its log. - In the terminal UI, a reply streamed while the screen is still behind the task's log no longer lands above the tool calls of the reply before it. - **The footer shows the effort whenever the task has one**, on a model outside the catalog too (it was sent but not shown); `/status` and the `/effort` picker say when none is sent. - **macOS**: background commands (`run_in_background`, Monitor) started: the log directory was made with `chmod 700 -- dir`, an order BSD `chmod` refuses (`chmod -- 700 dir` now). The same for the directory of pasted images. - **macOS: a read deny rule now refuses a shell command that names its file by a glob** (`cat secret.t*`, `grep key *`). The paths a command touches were expanded and resolved with GNU `realpath -m -z`; where that failed the globs went unexpanded and the rule matched nothing. They are now expanded wherever bash runs, and resolved one by one without GNU `realpath`. - **macOS: a background command dies with its worker**, as on Linux. Its guard killed the process group with `kill -KILL 0`, which on macOS ends the sender alone; the group's other processes are now killed one by one first. - **A server on this machine is reached directly, never through a proxy**: a noeta server, a local model, an MCP server, a hook URL. httpx took macOS's system proxy without its exceptions (localhost among them), and the proxy answered 502. - Memory files in an exec env elsewhere (a container) are listed without GNU `find -printf`, which BusyBox's `find` (Alpine images) lacks too. - The sandbox's network check finds this host's own addresses in the routing table, not by looking up the host's own name, which on macOS can wait on multicast DNS for longer than a connection may take (sandboxed connections and Monitor timed out). - The macOS CI job is a gate now and runs the terminal UI's tests too. ## [1.1.0] - 2026-11-09 One plugin across the kernel and every front end: a plugin is one name and one switch, and may carry UI data every front end shows and a web part the web and desktop apps load; it can be switched off for one task or for new tasks from the terminal, web and desktop apps; plugins install from marketplaces, the official one configured by default. ### Added - **A plugin can be switched off for one task.** The task config key `plugins_off` (set when the task is created or with the `config` signal) names plugins the task runs without from its next turn on, on whichever worker runs it: they keep folding their records but offer no tools, context, reminders, gates, commands or UI, and their timers (`.`) wait until they are back on. Sub-agents start with what their parent's turn runs with; a fork, a rewind or a conversation continued as a new task keeps its source's switches. `~/.noeta/plugin-switches.json` (`{"off": [...]}`, written by `PUT /plugins/{name}` and the front ends' plugin menus) is the machine's default for new tasks. A request may not switch off a built-in plugin a task is built on (the permission gate, the file and shell tools), nor one whose `config.toml` table says `switchable = false`. - **Plugins show UI in every front end.** `Extension.ui(view)` returns `StatusItem`, `Widget` and `Notice` items (exported from `noeta`), served by `GET /tasks/{id}/ui` and `AppAPI.ui`; `Extension.ui_call` answers a plugin's own front-end code (`POST /plugins/{name}/call`, `AppAPI.plugin_call`); a call it does not answer raises `UnknownUiCall` (`404`), any other error is `500`. `examples/hello-plugin` shows a status item. - **Plugins are listed and managed over the API.** `GET /plugins` (`AppAPI.plugins`) lists each plugin's source, version, parts, switch and why it is broken; a plugin's `.noeta-plugin/plugin.json` manifest may name a built web module (`ui_api`, `web`), served at `/plugins/{name}/web/` without a token. `POST /plugins/install`, `DELETE /plugins/{name}` and `GET /market` install, remove and discover plugins; installing and removing over HTTP are allowed only on a loopback server with no `--allow-host` / `--allow-origin` (a proxy in front) or with `noeta serve --allow-plugin-install`. `GET /market` also says why a marketplace could not be read, and lists its last reading then. - **Plugins' web parts in the web and desktop apps.** A web part is a built ES module exporting `activate(ctx)` that adds React components to the page's slots (`statusbar`, `sidebar`, `settings`, `session.header`, `composer`, `tool:`, `request:`, `record:/`, `codeblock:`, `theme`); the page provides `react`, `react-dom` and `@initxy/*` through an import map, which the server's content security policy allows by hash. A part that throws or waits in vain for a slot is named in the plugins dialog and leaves the page working. `web/sdk` (`@initxy/noeta-web-sdk`: `ctx` types, slot names, a build template) and `web/ui` (`@initxy/noeta-ui`) are the packages a part builds against (not yet published to npm); `examples/web-panel` is a plugin with one. In the desktop app `ctx.desktop` is its bridge. `activate` may be async; what a part subscribed to through `ctx` ends with it, and a rebuilt part (a new entry hash) is loaded again. - **The web and desktop apps' plugins dialog** (the header button or `/plugins`): Installed switches a plugin for this session or for new ones and removes it; Discover installs from the marketplaces, or from a typed-in path or pinned repository URL, after showing its source, commit and parts. Plugins' status items, widgets and notices show around the composer. - **The terminal UI's `/plugins`** switches a plugin for this task (from its next turn on) or for new tasks, removes one, and installs one from the marketplaces after showing its source, commit and parts. Plugins' status items show in a line under the footer, their widgets over or under the prompt and their notices in the transcript. - **Marketplaces you configure once.** `noeta plugin marketplace add|list|remove` keeps a list in `~/.noeta/marketplaces.json`: a path, `@`, or a git URL read at its default branch (the listing is data; each entry still installs only the commit it pins). The official marketplace, `noeta-plugins` (`https://github.com/initxy/noeta-plugins`), is configured until removed. `noeta plugin add ` installs a plugin a configured marketplace lists; `--marketplace` also takes a configured marketplace's name. An entry may carry a `noeta` object (`ui_api`, `parts`). `noeta.cli.plugin_command.market_entries()` lists every entry for front ends. A broken `marketplaces.json` is a notice (the official marketplace is used), install records are written atomically under a lock, and no other marketplace may take the name `noeta-plugins`. - A `config.toml` `[plugins.]` table naming a plugin not installed here is a notice at the start. ### Changed - **The web app's views of built-in plugins are those plugins' web parts** (`web/src/plugins//`), loaded through the same `ctx` and slots a third-party part gets: the task list, the permission mode picker and Shift+Tab, plan review, the auto-compact countdown (now in the status bar), the title, the file and shell tool cards, sub-agents, workflow runs, findings, and the Settings tab's memory, MCP and hooks sections. Switching one off for a session takes its view away; a tool no plugin shows gets a plain card. - **`repetition_guard`, `examples/chat-bridge` and `examples/chat-approvals` moved to the official marketplace** (`initxy/noeta-plugins`). A `config.toml` that sets `[plugins.repetition_guard]` or adds it to an agent now starts with a notice naming `noeta plugin add repetition_guard`; installed, the guard joins every agent with its arguments from that table, as before. A copied `chat_bridge.py` / `chat_approvals.py` keeps working. - A marketplace install without a plugin manifest is named by its marketplace entry (it was the fetched directory's name). ### Fixed - **The terminal UI opens about twice as fast** (2.9 s to 1.2 s here). The version a fold snapshot carries read and parsed the same source files once per plugin; each file is now parsed once and only its statements are walked (the version is the same). The source of each plugin's package was read from its metadata file every time it was asked for. - **A setting noeta cannot use ends the start with one line naming it**, not a traceback: a `config.toml` that is not valid TOML (with its line and column), an unknown provider, a plugin given an argument it does not take, a store URL of no known kind. - **What noeta logs while the terminal UI is open is shown in it.** Its screen covers stderr, so a warning logged at the start (a settings file that cannot be read, a model listing that failed) flashed by before the screen came up, and one logged later (an MCP server's settings, a hook's, a skill file's) was never seen. Each is now a line in the transcript, once; errors in the error color. `FrontEndStart.warnings` (`noeta.host.app.LoggedWarnings`) holds them for a front end. - **A paste goes into the terminal UI's prompt once** (it went in twice). - **What is typed or pasted while the transcript has focus** (after a click on it) goes to the prompt, as in Claude Code; a card's choices and the agents panel keep their keys. - **Replies read better in the terminal UI.** Their blocks are one blank line apart (two paragraphs ran together; a heading had two blank lines above it and an H1 was centred under its bullet), headings are bold in the text colour (not underlined in the accent colour, like a link), inline code is one soft colour, a code block has no blank padding, a table's header is in the text colour. Chinese and Japanese text breaks between characters, never before a comma or full stop: a line broke at the last space before a run of it, leaving most of the line empty. ## [1.0.6] - 2026-11-07 Open-source readiness: a restart limit for tasks that keep killing their worker, a Docker image for many workers, requests that no longer present noeta as Claude Code, a clearer first run, and DeepSeek's current models. ### Added - **A task whose run keeps killing its worker ends instead of taking down every worker.** A pickup after a claim that was never given back (its worker died, was killed or lost its lease mid-run) is recorded in the task's log (`core/reclaimed`); after three such restarts in a row with no step finished in between, the next crash ends the task `failed` with `restart_limit`, which front ends show like any failure and a sub-agent's parent hears as a failed child. A healthy task that loses its machine once resets at its next step; a worker shutting down cleanly is not counted. `noeta worker --restart-limit N` (`Runtime.restart_limit`, `0` for no limit) changes it. Workers older than this release leave a task with such a record to newer workers. - **DeepSeek's current models in the catalog**: `deepseek-flash` and `deepseek-v4-pro` (1M window, 384K output, list prices), so they start without the unknown-model warning and their cost is counted. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on 2026-07-24; the docs' examples use the new names. - **The Python API reference says what semver covers**: the top-level `noeta` names, the modules it lists, the plugin interface, the event types and the entry-point groups; everything else is internal. A public name is deprecated with a `DeprecationWarning` for at least one minor release, and only a major release removes it. - **CI runs the test suite on macOS** (Python 3.12). It reports without gating for now: its first run had failures, most in background shell jobs. - **Each release gets a GitHub Release** with its CHANGELOG section as the notes, created by the release workflow after publishing. - **A Docker image and a multi-worker compose file** (`deploy/`). `deploy/Dockerfile` builds noeta from the checkout with the `postgres` and `server` extras (locked versions), `git`, `ripgrep`, `bubblewrap` and `tini`, as a non-root user; `deploy/compose.yaml` runs Postgres, `noeta serve --no-worker` with a bearer token, and three workers sharing the workspace and noeta's home at the same paths. New page: [Deploy with Docker](https://initxy.github.io/noeta/guides/docker), with what the sandbox needs inside a container. CI builds the image (never pushed) and checks the compose file. ### Changed - **Requests to Anthropic no longer present noeta as Claude Code.** The `claude-code-20250219` beta is no longer sent, and `metadata.user_id` is this machine's opaque id (`~/.noeta/device_id`) instead of Claude Code's JSON of device, account and session ids. The protocol is still Claude Code's; both differences are listed in `parity/deviations.md`. - **Store adapters: `Store.claim` reports `Claim.reclaimed`**, true when the claimed row still had a lease (the claim before was neither released nor handed back by an append), read in the claim's transaction. A third-party store that does not set it keeps working, without the restart limit; the store contract tests check it. - **`mini-racer` is optional: install `noeta[workflow]` for the `Workflow` tool.** The embedded V8 (about 80 MB) was a hard dependency used only by Workflow scripts. Without it the tool refuses and names the install line, and `noeta plugins` names the missing extra. The `tui` extra does not pull it. - **A first run says what is missing.** With no model, noeta names a model and an API key, the minimal variables for Anthropic and for an OpenAI-compatible provider, and the quickstart. A missing key now stops the start before any request (it used to read the Anthropic model listing with an empty key and print a 401 first), and a model that does not look like Claude on the default provider gets the `NOETA_PROVIDER=openai_compat` / `NOETA_BASE_URL` hint. - **Development status is Beta** (`4 - Beta`) on PyPI, for `noeta` and `noeta-tui`. - **README and docs home lead with self-hosting on many workers**: one Postgres store, any worker continuing any task, waits that hold no process; Claude Code compatibility is shown with the `make parity` replay. The benchmark is shown with its standard error (+5.4, SE 3.6, not significant, on DeepSeek V4.1 Flash for both agents) instead of a bolded winner. - **The web and desktop apps are marked experimental**: built from source, not published, not in CI, installers unsigned. ## [1.0.5] - 2026-11-06 Fixes from a full review: compaction that looped, small windows, workflow approvals, sessions taken by `noeta serve`, the terminal UI, sandbox `denyRead`, and more. ### Fixed - **Compaction stops instead of looping.** When what survives a compaction already overflows the window, the turn ends with `autocompact_thrashing` after three refills instead of compacting forever; the overflow path and the automatic path share one guard. - **Small windows work.** The summary and buffer reserves shrink to half the window when they would take more (windows above ~66K keep Claude Code's numbers), so a 32K model no longer fails every turn. The error now says what didn't fit. - **A reply stopped after only whitespace** no longer leaves an empty assistant turn that made every later request fail with 400; old logs that hold one replay without it. - **Read shrinks PDF pages** to the image limits, so reading many slide pages is not rejected. - **Approvals from later workflow stages show up** in the terminal UI, ACP, stream-json and the web. - **A continued task (fork, /rewind, `--fork-session`) no longer inherits its source's sub-agents**, which had made them look forever running, blocked new agents and could delete a live worktree. - **`noeta serve` / `noeta worker` no longer pick up tasks created by the terminal UI, `-p` or ACP** on the same store; those go to a `front-end` queue that only their own front end runs. When someone acts on such a session through the server (a message, an answer, a cancel, a rewind) and no front end holds it, the server takes it over (`Store.requeue`, a new required store method); a front end that holds it keeps it. - **`/rewind` works with `noeta --server` and over ACP to a server**: `AppAPI.rewind` goes through the server's rewind endpoint instead of creating the task with server-only config. - **Children started with `&` in a foreground Bash call stay alive** after the call returns. - **Remote MCP requests use the call's own timeout**, not httpx's 5 seconds. - **One bad file in a plugin** (bad encoding, broken link, wrong-typed JSON) is skipped with a warning instead of stopping every entry point. - **The terminal UI honours `--settings`, `--add-dir`, `--mcp-config`, `--agents`, `--allowedTools`, `--disallowedTools` and `--dangerously-skip-permissions`**; `-p`-only flags are reported as ignored, and `--server` rejects flags that only shape a local task. - **Sandbox `denyRead` keeps a denied directory that contains a writable one** and re-binds the writable directory inside it (the Python install is re-bound read-only so the sandbox starts). - **The terminal UI**: a reopened task shows answered approvals as history instead of live cards that ate the next input; control sequences in tool output and model text are stripped before they reach the terminal; text with `[/…]`, non-UTF-8 `!` output and server errors (409, 403, connection) show a message instead of exiting; `/resume` lists every session, however many sub-agents there are (`Store.tasks(roots_only=True)`, `GET /tasks?roots_only`). - **Frontmatter with CRLF line endings or a BOM** is read correctly everywhere (one parser in `noeta/toolkit/frontmatter.py`); a rules file no longer loses its `paths:`. - **`git diff | noeta -p "review this diff"`** appends piped stdin to the prompt, as Claude Code does. - Docs: what leaving the sandbox does in auto and bypass, the absolute sqlite URL in the embedding guide. ## [1.0.4] - 2026-11-05 ### Fixed - **noeta starts behind a SOCKS proxy.** With `ALL_PROXY=socks5://…` in the environment (Clash and similar tools set it), `noeta` failed at start with `Using SOCKS proxy, but the 'socksio' package is not installed`: noeta now depends on `httpx[socks]`. ## [1.0.3] - 2026-11-05 The terminal UI lists sub-agents as Claude Code does and draws in the terminal's own colours. ### Changed - **The terminal UI lists sub-agents under the footer, as Claude Code does**: `● main`, then one line per agent (a workflow is one line, `(+N)` for its agents) with what it said or the tool it runs, how long it has run and its tokens; agents that ended stay until the next message. `↓` from an empty prompt (or Ctrl+B, which replaces the side panel) picks one, and `Enter` or a click shows what that agent did in the transcript's place, following it while it runs; `Esc` goes back. An approval or question brings the transcript back. - **The terminal UI draws in the terminal's own colours**: its background (an image or transparency too) and text colour show through instead of Textual's dark grey; borders use the terminal's grey and secondary text its dim style. The light variant is picked when `COLORFGBG` says the background is light. noeta's own colours come from its mark: green for the name and what is picked, amber for what is working (the spinner, a running agent). - A tool call whose argument is too long for a line wraps under its name instead of leaving the `●` alone on a line. ## [1.0.2] - 2026-11-05 A reworked terminal UI, and the README and package pages written for an open-source audience. ### Changed - **The terminal UI, reworked.** The welcome box shows noeta's mark beside the version, model and directory. Background agents show what they are doing (`Bash(ls)`, `3 tool uses · 1.2k tokens · 12s`) and then how they ended, instead of the launch text written for the model; a `Workflow` call shows its agents per phase and the run's end, not a notice per agent. The model's reasoning streams into a `∴ Thinking…` line (ctrl+o shows it) and counts toward the spinner's tokens. The footer carries the effort, a context bar, the cost and the directory (what does not fit is left out) and how many agents run in the background; ctrl+b lists them. The prompt sits in a box; shift+enter (in terminals that report it) adds a line; dropping several image files attaches them all, listed over the prompt; ctrl+v reads the clipboard's image on macOS without `pngpaste`. - **README and package metadata for an open-source audience.** The README opens with the logo, a screenshot, the headline features and a short benchmark table, with a Chinese edition (`README.zh-CN.md`); links are absolute so PyPI renders it. The PyPI description, keywords and classifiers say "Claude Code-compatible". - **Bench and eval read their model from a git-ignored `.env`**, with no internal model id as the default. ### Fixed - **The terminal UI's transcript no longer sinks to the bottom** of the screen when the first reply streams in while it is shorter than the screen. - **The slash menu keeps one height** and cuts long descriptions to a line, so typing no longer moves the prompt; it takes the footer's place while open. - **Wrapped tool output lines up under its text**, not under the `⎿`. - **A running call's first output is no longer lost** when the tool reports it before the terminal UI has shown the call. ## [1.0.1] - 2026-11-05 The first stable 1.0 release. The terminal UI is on PyPI too: `noeta` and `noeta-tui` are released together, always at the same version, so `uv tool install 'noeta[tui]'` (or `pip install 'noeta[tui]'`) is the whole install. ### Changed - **`noeta-tui` is published with `noeta`.** The release workflow builds both, checks that both wheels carry the tag's version and install together, then publishes both. The `tui` extra pins `noeta-tui` to noeta's own version and `noeta-tui` pins `noeta` back; a test keeps the two `pyproject.toml` files in step. - **Install hints name PyPI.** A missing extra or terminal UI now prints `uv tool install --reinstall 'noeta[]'` / `pip install 'noeta[]'` instead of a `git+https` requirement. - **Docs install from PyPI.** README, the quickstart and the terminal, server, front-ends and plugins guides use `noeta[...]` instead of `git+https` lines. The package's Documentation link is the docs site. ## [1.0.0rc1] - 2026-11-05 The first 1.0 release candidate, published as `noeta` (the 0.x packages were `noeta-runtime` and `noeta-sdk`). The terminal UI, `noeta-tui`, is installed from the repository for now. ### Changed - **Pillow is a dependency.** Shrinking large images is no longer an extra to remember: the `images` extra is gone. The terminal UI is the `tui` extra: from a checkout, `uv tool install '.[tui]'` is the whole install line. ### Added - **The terminal UI's tasks get Claude Code's `! ` line.** The main prompt's `# Session-specific guidance` now opens with Claude Code's rule to suggest `! ` for a command the person must run themselves (an interactive login), given only to a task whose config says its front end runs `!` commands: `bash_mode: true`, which the terminal UI sets on the tasks it creates in process (not with `--server`, where it refuses them) and an HTTP request may set. Sub-agents do not get it; a fork keeps it with its parent's prompt. Every other request is byte-identical to before. `drop_skill_guidance` is now `noeta.toolkit.guidance_lines.drop_guidance_line`, shared by both lines. - **An output cap the window cannot hold is lowered, not compacted.** A provider's "input length and `max_tokens` exceed context limit" (Anthropic's, and OpenAI's and vLLM's forms that state the input and the window) retries the call once with the cap the window leaves (less 1,000 tokens, at least 3,000), as Claude Code does; other overflows still compact. - **A sub-agent's long result is saved, not inlined.** A result over 100,000 characters in its completion notice, or a hand-back report that long, reaches the caller as a preview of the file it was saved to (start and end without a spill directory). A parked tool's resumed result is held to the same result limits as one returned at once. - **One-hour prompt cache for Anthropic**, off by default: `prompt_cache_ttl = "1h"` in `config.toml` (top level or an `anthropic` `[providers.]` table) marks every cache breakpoint `{"type": "ephemeral", "ttl": "1h"}`; its replies carry `ModelResponse.cache_ttl` (3600), folded into `TaskState.cache_ttl` for plugins. - **Long-context options of the compaction plugin** (Claude Code's): a summary written in the background from 80% of the window and swapped in at the threshold (`auto_compact_precompute`, on); older results of clearable tools cleared after an idle gap longer than the prompt cache lives (`auto_compact_clear_results`, off); with the one-hour cache, a large idle task compacted before its cache expires (`auto_compact_idle`, off). - **Session search and archive in the web and desktop apps.** The sidebar's search box narrows the sessions by title, first prompt or folder; a row's button archives a session, and the "Archived" list brings one back. Archiving is the `archive` signal (`{"archived": bool}`, allowed over HTTP), read from the task's inbox so an ended task can be archived too; `GET /summaries` gives each root's `archived` and takes `archived=true|false`; `AppAPI.summaries` / `archive` (`LocalApp`, `RemoteApp`). Sessions are not deleted: the store keeps every task's log. - **The terminal UI is its own package, `noeta-tui` (`tui/`)**, found by `noeta` through the entry-point group `noeta.frontends`; the `tui` extra is gone (install line in the README). It now looks and works like Claude Code 2.1's: welcome box, `❯` prompt between rules, mode line and shortcuts panel, spinner, folded `⎿` results (`ctrl+o`), numbered diffs, the task list, queued messages, a numbered approval dialog, slash and `@` menus, `!` bash mode, pickers for `/model`, `/resume` and rewind, `/status`, `/permissions`, `/agents` and a `/context` grid (`tui/screenshots/README.md`). - **Claude Code's task list as the plugin `todos`** (spec `2026-11-02-task-tools`): `TaskCreate`, `TaskGet`, `TaskUpdate`, `TaskList` (deferred) and a reminder after ten model turns without them, for the models Claude Code 2.1.312 gives them to: older Claude models (Claude 3.x, Opus 4.0-4.7, Sonnet 4.0-4.6, Haiku 4.5, by Claude Code's canonical name, so `claude-opus-4-20250514` too) and Bedrock application inference profiles; the Claude 5 family and every other model (DeepSeek, a gateway's routing name) get none. The main agent runs it; `[plugins.todos] all_models = true` offers the tools to every model, `enabled = false` to none. The list is a fold of the task's log (any worker, after a compaction, in a fork); sub-agents get no task tools. Those models' system prompt gains Claude Code's one task-tools line. ### Fixed - **A foreground `Bash` command's output is kept on Ubuntu 26.04.** Its coreutils are uutils, whose `dd` empties its output file when it reads nothing, so the keeper's last read wiped the log and every foreground command came back "(Bash completed with no output)". - **`noeta worker` sweeps old saved tool outputs**, daily, as `noeta serve` does (`LocalApp.run_until`); a worker-only deployment used to keep them forever. - **No permission asked for `CronCreate`, `CronList`, `CronDelete`, `EnterWorktree`, `ExitWorktree` or `ReadMcpResourceDirTool`**, as in Claude Code (`CronCreate` still goes to the auto-mode classifier). - **Skills in `~/.agents/skills`** (the directory coding agents share) reach the `noeta` command, below `~/.noeta/skills`. - **Cron jobs run in the task's time zone** (task config `timezone`), not the worker machine's; without one, the machine's as before. ### Changed - **Holds are gone; a task with a background job keeps its own claim** (ADR-029, spec `2026-11-04-v1-finish` D1): `Store.hold` / `unhold` / `reap_holds`, the `tree_holds` table, `hold_lost`, `HeldElsewhere`, `UnservedQueue`, `Runtime.hold`, `ToolContext.hold` and `Post.forward_if_ended` are removed. A tool keeps its task's claim with `ToolContext.keep()` (`KeptClaim`); the task waits with `Patch.keep`, its run goes on in the background (`Runtime.run_claim` returns, no slot taken) and takes its own inbox; `Runtime.let_go` gives the claims up on the way out. No background job is refused for where tasks run any more. A sub-agent's jobs die with it (they used to report to the root); `TaskStop` reaches a running child's job through the child's inbox. A worker that dies loses its jobs; the next worker to claim the task (at once, the kept lease having run out) tells the model they are lost. **Breaking** for code that called the removed names. - **Store schema version 5** (D4): a 0.6 store (version 1) is upgraded in place (its `holds` table dropped); the development-only upgrades from versions 2 and 3 are gone. - **Postgres claims and wakes** (D5): the claim index holds only claimable rows (not idle waiting tasks); `Runtime.serve` looks for work only after a change that can make a task claimable (`created`, `posted`, `released`, now also reported by an append that leaves its task ready); a running step no longer polls its inbox every 0.5 s on a store whose change feed reaches every process (`shared_changes`, Postgres). - **Removed `WriteTool(allowed_path_globs=)`**: nothing reached it (no plugin argument, agent field or setting); permission rules confine writes. - **A tool's optional flags are one typed `ToolFlags`** (`noeta.core.tool`): `deferred`, `search_hint`, `loaded_description`, `listed_last`, `run_last`, `past_agent_lists`, `retry_safe`, `silent_reply_after`, `unchecked_input`, `safe_while_planning` and `max_result_chars` are set as `flags = ToolFlags(...)` and read with `tool_flags(tool)` (which replaces `tool_flag`); a misspelt flag is now a type error instead of a silent default. A third-party tool that set one as its own attribute sets it in `flags` instead. - **Cross-plugin reads go through `noeta.toolkit.plugin_records`**: one typed reader per record a plugin publishes (`permission_mode`, `shell_cwd`, `memory_dir`, ...), the methods one plugin calls on another (`mode_of`, `working_directories`, `read_deny_rule`, ...) and the plugins' names. `PLAN_MODE_PLUGIN` left `toolkit.permission_modes` (`plugin_records.PLAN_MODE`). - **A tool name two plugins offer is logged** (once, a warning naming both); the first still wins. - **A write outside the working directories is a permission question, as in Claude Code**: a `Write`, `Edit` or `NotebookEdit` outside the working and additional directories was refused in every mode; now `default`, `acceptEdits` and plan mode ask (with an "always" rule for its directory), `bypassPermissions` lets it through, `auto` asks the classifier and `dontAsk` refuses. Deny rules, ask rules, allow rules and protected paths apply as before. A host that wants the old fence sets `[plugins.fs] block_writes_outside = true`: such a write is then refused in every mode before anyone is asked. A rewind restores such a file too. - **Plugin splits** (spec `2026-11-03-plugin-splits`): parts of the permissions plugin and the kernel became built-in plugins of their own, each switchable with `[plugins.] enabled`. - **Plan mode is the plugin `plan_mode`** (on by default in every built-in agent): `/plan`, `EnterPlanMode` / `ExitPlanMode`, the plan file and its name, the plan-mode notes. With `[plugins.permissions] enabled = false` plan mode still plans read-only (every call of a tool that is not read-only, the plan file's write aside, is asked about, shell commands included) and `ExitPlanMode` still asks the person; with `[plugins.plan_mode] enabled = false` there are no plan tools, no `/plan`, no plan-mode notes, and `plan` is neither offered to front ends nor taken as a switch or a starting mode. The mode stays the `permissions` `mode` / `plan_exit` records front ends read; a log written before the split replays the same. - **Breaking**: `Permissions(auto_during_plan=...)` is `PlanMode(auto_during_plan=...)` (`useAutoModeDuringPlan` in the settings files goes there); a host building agents by hand adds `PlanMode(plans_dir=...)` next to `Permissions` to keep plan mode. - **The budget caps are the plugin `budget`** (`BudgetCaps`, on by default in every built-in agent, as Claude Code's SDK has `maxTurns` / `maxBudgetUsd`): `max_iterations` (or `max_turns`), `max_tool_calls`, `max_cost_usd`, `max_spawned_subtasks`, `max_subtask_depth` (default 3), `spawn_tools`, set in `[plugins.budget]`. They hold with the permissions plugin off; a log from before the split keeps its counts. **Breaking**: `Permissions(budget=..., max_turns=..., spawn_tools=...)` and `noeta.plugins.permissions.Budget` are gone. - **The repetition guard is the plugin `repetition_guard`** (`RepetitionGuard`: `threshold` default 3, `action` `require_approval` or `deny`, `window` 8). Claude Code has none, so the built-in agents keep it off until a host sets `[plugins.repetition_guard]` (it was off by default before too: `repetition_threshold=0`). Its ask goes to the person, not to the permissions plugin's `can_use_tool`. **Breaking**: `Permissions(repetition_threshold=..., repetition_action=..., repetition_window=...)` and `noeta.plugins.permissions.RepetitionPolicy` are gone. - **The empty-reply nudge is the plugin `empty_reply`, on by default.** The kernel no longer asks the model to go on after a reply with no text and no tool call: a new extension hook, `Extension.on_empty_reply(view, response)`, returns the message to go on with (the core records it in the same `continue` entry, reason `empty_reply`), and with no answer the turn ends quietly (`turn_done`, reason `empty_reply`, the reply's usage kept). The plugin keeps Claude Code's behavior unchanged: once per step, not after a `Tool.silent_reply_after` tool. Every built-in agent and `DEFAULT_PLUGINS` run it; `[plugins.empty_reply] enabled = false` turns it off, and then the first empty reply ends the turn. Old logs replay unchanged. `EMPTY_REPLY_NUDGE` moved from `noeta.core.runner.texts` to `noeta.toolkit.empty_reply`. - **Breaking, for hosts that list plugins themselves**: an agent file with its own `plugins:` list, and an SDK host building `Permissions` by hand, lose plan mode, the sub-agent depth cap of 3 and the empty-reply re-ask unless they add `plan_mode`, `budget` and `empty_reply` (`PlanMode`, `BudgetCaps`, `EmptyReply`). - New tool flag `safe_while_planning` (set on `Agent`, `SubagentHandback`, `SendMessage`, `AskUserQuestion`, `Skill`, `TaskStop`, `ScheduleWakeup`, `Workflow` and the task-list tools): plan mode without the permissions plugin lets these run, as the permissions plugin does. - The kernel ends a turn after 3 empty replies in a row whatever `on_empty_reply` says, and an extension whose `on_empty_reply` fails has no opinion. - The permissions plugin's `can_use_tool` and classifier answer its own ask on a call that another plugin (the repetition guard) also asks about; that ask then goes to the person. - New extension mechanism: `Extension.folds` names other record names whose records also reach the extension's `reduce` (how a split-out plugin keeps reading its old records). - **Context final closure: the third audit's differences from Claude Code 2.1.312 are matched or written down** (spec `2026-11-02-context-final-closure`; kept differences are rows in `parity/deviations.md`). - **Breaking** (what a host or an existing task may notice; each follows Claude Code): - Every call's input is checked against its tool's schema before any permission check; a misfit (a missing or unexpected argument, a wrong type, a value outside an enum) is refused as `InputValidationError: …`. MCP tools, `function_tool` tools and the answer tool are not checked (`Tool.unchecked_input`); a `null` for an optional argument counts as leaving it out. A value that fits only through Claude Code's coercions (`"false"`, `"5"`, JSON text for an array) reaches the permission checks and the tool converted. - Errors that refuse a call before the tool acts come back inside `` with no tool-name prefix (an unknown or not-loaded tool, `Edit`'s string not found, a file not read first, a missing file, a task id that names nothing running); an unknown tool reads `Error: No such tool available: `. Failures while a tool works keep `: `. - Instruction files (and the memory index) a `Read` deny rule covers are not loaded; the person gets a notice per file. - Haiku 4.5 (a catalogued Claude model without adaptive thinking) is sent `thinking: {type: "enabled", budget_tokens}` when thinking is on and `max_tokens` is over 1,024 (not while a tool loop begun without thinking goes on); it was sent no `thinking`. Agent hooks' requests go without thinking. - A saved "always" rule adds `.noeta/settings.local.json` to the repository's `.git/info/exclude`. - A fork starts with a `` frame around its directive, and its copy of the starting `Agent` call says the fork is itself. - A whole `Read` of an instruction file shown and unchanged says it is already in context; the repeated-read note calls the read wasted. - The post-compaction restore skips the instruction files shown, the memory index and the plan file by exact path (it skipped any `NOETA.md` / `AGENTS.md` by name and restored the plan file). - MCP servers that failed to connect or wait for a sign-in are told to the model when that set changes; several SessionStart contexts are one message; a bare unknown `/command` gets a note that nothing ran; `EnterPlanMode`'s description is complete. - **Context re-audit closure: a second, independent audit's differences from Claude Code 2.1.312 are matched or written down** (spec `2026-11-02-context-reaudit-closure`; kept differences are rows in `parity/deviations.md`). - **Breaking** (what a host or an existing task may notice; each follows Claude Code): - Models: the default `max_tokens` is the model's catalog default, not its ceiling (Opus 5, Sonnet 5 and Opus 4.8 128K → 64K; Sonnet 4.6 64K → 32K); a reasoning model whose row names no `default_effort` is asked for `high`, not `medium`; Sonnet 4.6's window is 200K, not 1M (Claude Code's without its 1M option; compaction starts near 187K, and noeta has no 1M option for it); Fable 5.1's ceiling is 128K, which a `compaction_model` on it now gets as `max_tokens`. Non-Claude models no longer get the quiet nudge. `WebSearch` without `mode` runs `extended`. - Hooks: a `UserPromptSubmit` hook's `continue: false` ends the turn as done with the prompt kept (it failed the turn); a `PostToolBatch` block ends the turn (it was context for the model). - Task config: `ultracode` and `max_budget_usd` are new changeable keys. - MCP tool names keep the server's case and are no longer cut: a server `GitHub` gives `mcp__GitHub__delete_repo` where it gave `mcp__github__delete_repo`; a name over 64 characters is no longer cut and hashed (over 128 the tool is not offered). Settings that name a tool by its old name keep narrowing, never widening (each match logged once): deny and ask rules, `disallowed_tools` and an agent's `disallowedTools` still match old names; allow rules, remembered "always allow" grants, `allowed_tools` / `--allowedTools`, an agent's `tools:` and a skill's `allowed-tools` match the current name only, so a tool named there by its old name is asked about (or not offered). A hook reached only by an old name (matcher, `if`, `mcp_tool` hook, `--permission-prompt-tool`) may block, deny or ask, but its allow is ignored; an old name two current tools share resolves to neither. Migration: rewrite allow entries and hooks with the current name (`mcp__GitHub` for `mcp__github`); the warning names each. - Skills are named by their directory, not their frontmatter `name`. The former name still answers `Skill(...)` permission rules (deny and ask always; allow while no other skill had that name), hook `if` conditions (allowing only while unambiguous), `Skills(preload=...)` and an agent file's `skills:` (a warning names the new name). Only `//SKILL.md` is a skill; a deeper one is skipped with a warning. The user's skill directories win a name over the project's. Migration: rename rules, hook conditions and preloads to the directory name; move deeper skills up one level; rename a project skill that shares a user skill's name. - Plan mode: a plan begun in auto mode keeps the auto-mode classifier for what plan mode would ask (`useAutoModeDuringPlan`, default on; plugin argument `auto_during_plan`); file writes stay with the person, and a shell command plan mode would ask about runs on the classifier's word. A plan begun in any other mode asks the person as before, with no auto-mode betas or classifier calls. With `auto_during_plan=False`, entering plan mode from auto mode ends auto mode. - `Permissions(auto_mode=...)`: each of `allow`, `soft_deny`, `hard_deny` and `environment` replaces the built-in text of its classifier section, as Claude Code's `autoMode` does. A list written the old way (`soft_deny: ["never touch prod"]`) drops the built-in blocks such as force push and `rm -rf`; a `hard_deny` / `soft_deny` list without `"$defaults"` is logged at load. Migration: put `"$defaults"` where the built-in rules go (`["$defaults", "never touch prod"]`). A custom classifier prompt's `<>`, `<>`, `<>` and single `<>` slot are no longer filled: wrap each section's built-in text in `<>…<>`, `<>…<>`, `<>…<>`, `<>…<>` (`<>` is unchanged). - A new core record `spent` (a compaction's summarize calls) counts in a task's usage; a worker running an older version hands a task whose log holds one to a newer worker (`NewerRecord`). Migration: upgrade every worker of a store together. - Public names: `noeta.toolkit.side_requests.DEFAULT_EFFORT` is gone (use `noeta.core.models.default_effort(model)`); `MEMORY_INDEX_MAX_BYTES` → `MEMORY_INDEX_MAX_CHARS`, `INDEX_MAX_BYTES` → `INDEX_MAX_CHARS`, `RECALL_TASK_BYTES` → `RECALL_TASK_CHARS` (characters, not bytes); `shell.sandbox.sandbox_description` → `sandbox_instructions`; the shell `Job.err_path` → `events_path`; `parse_skill` takes a required keyword `name=`; the skills frontmatter parser no longer raises on a missing closing `---`; the permissions plugin's `server_context` and `auto_request` context callbacks are async. `ChildEnded` gains `output_tokens` and `TaskView` gains `kept` (both with defaults). - Parity: `make parity` compares each request's tool list in order, and a deferred-tool list sent by only one side (it compared only a session's union of tools before). - Models: Claude requests send Claude Code's per-model default `max_tokens` and effort (for example Sonnet 4.6 32,000 / `high`), also on side requests (title, plan name, digest); every Claude row has a knowledge cutoff; Sonnet 4.6's window is 200K. New `ModelSpec` `default_output_tokens`, `default_effort` and `models.default_effort`. The Fable 5.1 identity and Haiku 5.5 early-stopping paragraphs; the quiet nudge only for Opus 5.5, Sonnet 5.5 and Fable 5.1; betas a request adds go in Claude Code's order. - Prompt and environment: the "# Session-specific guidance" skill line only with skills and the `Skill` tool; the model line uses the short name; the Shell line reads `bash (login shell: zsh)`; main-branch fallback `origin/main`, `origin/master`, `main`; the git status is trimmed. New notes Claude Code also sends: a model switch, told before the next call; linked-worktree and shared-stash lines; additional working directories; "# Environment update" for repository, worktree and directory changes; the `language` setting; `attribution.commit` / `attribution.pr` and `includeCoAuthoredBy`. MCP server instructions follow the agent list. - Reminders and modes: no empty-reply nudge after any `StructuredOutput` call or a tool in `NOETA_TERMINAL_MCP_TOOLS`; `ultrathink` (note, and `xhigh` for an interactive turn on a reasoning model) and `ultracode` (keyword note, task config `ultracode`); a USD budget note before each call with `--max-budget-usd` (task config `max_budget_usd`); `@agent-` mentions; `criticalSystemReminder_EXPERIMENTAL` in `--agents` definitions; an @-mentioned file already read and unchanged attaches nothing. Mode notes: the auto-mode shell-first note is sent again only after auto mode was left or a compaction, in `bypassPermissions` too (as Claude Code); an exit note when leaving auto mode; the plan-exit note on any way out of plan mode; none for agents without `Bash` plus `Edit`/`Write`. Two mode switches in one step both count. - Hooks: plain stdout of `SessionStart` / `UserPromptSubmit` is "hook success"; `continue: false` is told to the model and its stop is posted with the hook's records; a `PostToolBatch` block ends the turn; `async` / `asyncRewake` output reaches the model through the task's inbox; hook text over 10,000 characters is saved to a file with a preview. - Shell: a background `grep`/`rg`/`find`/`diff`/`test`/`git diff`/`git grep` exiting 1 ends `completed` with the meaning in its summary; a background command silent for 45 s behind a prompt-like line brings one notice; a Monitor's stderr goes to its output file and the end notice names it; `TaskStop` also stops the task's background agents and Workflow runs; the sandbox is described in a `sandbox_instructions` note (again when it changes, after a compaction, and once when it goes off), not in `Bash`'s description; outside `auto` and `bypassPermissions`, `Bash`'s description steers `cat`/`head`/`tail`/`sed`/`awk`/`echo` to the dedicated tools (decided once per task). A Workflow end notice names a dynamic workflow, gives the call that resumes a failed or stopped run (or re-runs a completed one), lists failed agents and reports agent count, tokens, tool uses and duration. - Tools: `DeferredToolPlaceholder` is always second to last. MCP tool names keep case and are not cut (a name over 128 characters is not offered; OpenAI-family adapters shorten names over 64 on the wire and map calls back). Tool search uses search hints (built-in, and MCP `_meta["anthropic/searchHint"]`) and Claude Code's whole-name scoring; a fruitless search names the MCP servers that are down. Per-tool `_meta["anthropic/alwaysLoad"]` and `_meta.ui.visibility` are honoured; a server's `alwaysLoad: false` defers every tool. `WebSearch` `mode` is optional (default `extended`) and its result uses Claude Code's frame; `ReadMcpResourceDirTool` uses Claude Code's result shape; `SendMessage` `to` is one line and `message` defaults to `""`. - Skills: a skill is named by its directory (its frontmatter `name` shown beside it), only `/SKILL.md` one level down, described by its first line when it has no description, and loaded with broken frontmatter ignored. Skills also come from the folders up to the repository root, the managed directory, the additional directories, `.noeta/commands` / `~/.noeta/commands` and a Claude Code plugin's commands; the first of a name wins (the user's beats the project's); a nested `.noeta/skills` wins over `.agents/skills`. Frontmatter `user-invocable`, and `model` / `effort` for the rest of the turn. Bodies fill in `${CLAUDE_SESSION_ID}`, `${CLAUDE_EFFORT}`, a plugin's `${CLAUDE_PLUGIN_ROOT}`, `${CLAUDE_PLUGIN_DATA}` and `${user_config.*}`. Later listings fit only new skills, ranked; after a compaction the listing is rebuilt (leaving out skills a kept listing still shows) and a skill given again is its text from when it loaded; usage counts once a minute per skill. Loading `verify` or `code-review` looks for scoped skill folders by git's changed files. New settings `skillListingBudgetFraction`, `skillListingMaxDescChars`, `NOETA_SLASH_COMMAND_TOOL_CHAR_BUDGET`. `NotebookEdit` no longer finds skills. - Memory and instruction files: `MEMORY.md` (25,000) and the task's recall budget (61,440) count characters, not bytes; only HTML comments standing as blocks are dropped; a nested file's header is its path; with each new message an instruction file that appeared is shown and one that is gone is named. - Compaction: a failed automatic compaction no longer ends the turn (the request goes out uncompacted; after three failures in a row automatic compaction is off for the task); a fallback summarize call (plain prompt, `Read`, with tool search also `ToolSearch` and the MCP tools, images as `[image]`, no cache marks) when the first errs or brings no summary; after a compaction the task's plan file and a status line per running background agent, shell or monitor are given again. - Sub-agents, plan mode, classifier: a flagged hand-back is delivered under a warning; an agent offered `SubagentHandback` that ends without it delivers no report; the completion notice has a `` element, says who stopped the agent, uses the step-limit wording and omits an empty ``, and `subagent_tokens` is the newest input plus all output (new `ChildEnded.output_tokens`); progress labels every 30 s; Explore and Plan see the managed instruction files, Explore under a Fable caller runs on Opus; `ExitPlanMode` without a plan file asks, a rejected plan comes back in the result; the Plan prompt gains the similar-features step and a read-only closing reminder. A classifier transcript too long for its window falls back to asking; a stage-2 error after a stage-1 block is a counted block; classifier requests carry the main loop's session id. - Classifier: `AskUserQuestion` calls are shown in Claude Code's form (header, multiSelect, numbered options with description and preview); the `safeguards` context names Claude Code's eleven settings sources, the exec env's platform, home, git root and branch, and `case_insensitive_paths` only on Windows; a sub-agent can no longer set the hand-back review key itself. - Hooks: a `UserPromptSubmit` hook's `continue: false` keeps the prompt, tells the model the operation was stopped by the hook and ends the turn without a model call (it failed the turn); a background `SessionStart` answer that repeats the latest `SessionStart` output is left out. `TaskStop` on a background agent: its notice says the caller stopped it. - Review fixes: hook output is capped only where it reaches the model (a long `PreCompact` output reaches the summary request whole; a failed save cuts inline; background hook failures are logged); the usage a host reads (`/cost`, the result message, HTTP `usage`, `TaskState.usage`) and the USD budget note include compaction calls, failed ones too, and the budget reads an ended task's log once; the `@agent-` and `ultrathink` notes are made once per prompt; a Monitor's output file stays within `log_max_bytes`, stdout and stderr each keeping half of it whatever order they come in; `Shell(background=False)` offers `TaskStop` too; a Workflow run stopped with `TaskStop` tells only the person; a skill's `model` / `effort` end with the turn that loaded it, whoever opens the next; a skill's `model` of another provider family (or unknown to the catalog) is ignored with a warning; a `.noeta/commands` file never takes a plugin skill's or a scoped skill's name; an instruction file that cannot be read for a moment is not called gone; the walk up for skill folders stops at the exec env's home and is kept per exec env; `skillListingMaxDescChars` must be a whole number; a four-backtick fence is not closed by three; on the Anthropic wire a `tool_reference` names only a tool the request offers (an old MCP name as the current one); on OpenAI-family wires a name over 64 characters is written short in the request's text too; MCP tools dropped for names over 128 characters are named by a fruitless `ToolSearch`; OpenAI-family wires rewrite only the request's own long tool names, never other `mcp__` text. A skill's `model` / `effort` also end when the turn's call fails or the turn is stopped; a forked or resumed sub-agent starts without the source's skill override and notes `@agent` mentions in its new prompt; a sub-agent can no longer suppress the hand-back warning by writing it itself; a sub-agent of a planning task plans as its parent does (new child config key `plan_origin`), whatever the service's default mode; an instruction file emptied (or left with only comments) is named once as no longer applying; `Budget.max_cost_usd` counts compaction calls and dropped replies, as `/cost` does; the skills walk-up stays put when a remote env's home cannot be read; saved hook output stays named when its directory cannot be marked; a nested command whose name is taken is warned about once; an `mcp_tool` hook that names, by its old name, a tool two servers now share blocks the event it guards instead of failing open (on `Stop` / `SubagentStop` it fails, so the turn ends). A task that continues a conversation fires SessionStart once with Claude Code's source: `fork` for `history_from` (HTTP, `--fork-session`), `resume` for a channel thread carried on (new config key `continued_as`), none for a rewind; what its hooks add reaches the model as at startup. Such a task (a fork, a channel continuation, a CLI or HTTP resume) runs its `UserPromptSubmit` hooks on its own prompts (they were skipped), attaches the files its prompts @-mention and saves their pasted images, and recalls memory for them; the walk up for skill folders on this machine is no longer cached. `make parity` reports a tool list missing from its data and compares cache marks and description changes per request. - **Context audit closure: what the model sees follows Claude Code 2.1.312 where it differed without a written reason** (spec `2026-11-02-context-audit-closure`; kept differences are rows in `parity/deviations.md`, which now has a pi / dsh column and no "Not yet matched" section). - Environment: the git checkout is found from a working directory below its top ("Is a git repository" true, git snapshot sent); the status is cut at 2,000 characters with a note; the Shell line names the `bash` commands run in (plus the login shell when it differs); the date-change note is checked before every model call; after a compaction the git snapshot is taken again. - Files: the changed-file note watches every remembered read file (up to 5,000) in one batch (new optional `ExecEnv.stat_many`; one `docker exec` per ~100 KB of paths on a container); UTF-16LE files are read and written in their encoding and a UTF-8 byte-order mark is kept; not-text files (UTF-16BE, early NUL bytes, invalid UTF-8) are shown by `Read` with a note and refused by `Edit` / `Write` / `NotebookEdit`; a file a read-only `Bash` command showed (`cat`, `head`, `tail`, `sed -n`, a matching `grep`) counts as read, brings the nested instruction files that apply to it, and is restored after a compaction; a command that rewrites files in place (a formatter, `sed -i`) ends its result with the read files it changed. - MCP: tool descriptions are cut at 4,096 characters plus `… [truncated]` (16,384 once loaded through `ToolSearch`; `NOETA_MAX_MCP_DESCRIPTION_LENGTH`); prompts, resources and the resource tools are no longer cut at 64 KB; a result between half and four times its budget is counted by the provider where it can; prompt commands keep embedded resource text. - `WebFetch` asks for Markdown first and returns a preapproved documentation site's Markdown without a digest; the digest request follows Claude Code's order and rules (quote cap, no verbatim copying, no legality remarks, no lyrics; looser rules for documentation sites). - Side requests (title, plan name, digest) open with the identity line, use the model's output ceiling and the task's effort, and leave `thinking` out on Anthropic models; a plan is named once approved. - Headless: `-p` and stream-json tasks are not interactive (task config `interactive`, `permission_prompt_tool`): no title request, `AskUserQuestion` only when the client answers prompts. `per-turn-control-2026-07-01` goes only to models with per-turn effort. A stream-json client's `initialize.hooks` run (`hook_callback`), and `--permission-prompt-tool mcp____` answers permission requests. - **Breaking:** structured output goes through a `StructuredOutput` tool (`--json-schema`, `initialize.jsonSchema`, the HTTP `output_schema`, workflow `agent({schema})`), with one reminder and a cap of five refused calls (`error_max_structured_output_retries`); the main conversation no longer sends the provider's `output_schema`, `Agent.output_schema` needs the `structured_output` plugin (every agent built from agent files has it), and `structured_answer()` returns the accepted call's value instead of parsing each turn's text. - `AskUserQuestion` may be left partly or wholly unanswered. - Skills: a Claude Code plugin's skills are `:`, its agents and commands `:[:]`; a nested skill that shares a name is offered as `:` (a skill file cannot name itself with a `:`, and a scoped name never replaces another skill); a typed `/` of a `context: fork` skill starts its agent with no model call; a skill given again after a compaction names its source. A skill's `` !`cmd` `` that would be asked about still refuses the load in every mode (a deviation: Claude Code's auto mode hands it over). - Sub-agents: prompts end with the token budget line; general-purpose sees its caller's `MEMORY.md` index; the auto-mode classifier for a sub-agent without instructions sees its root task's; an agent file's `omitClaudeMd` keeps the managed instruction files. - A background job's notice that arrives during a turn is a system turn after the tool results; Anthropic requests add Claude Code's newline where two user messages are joined. - Providers: an Anthropic stream that breaks after it began goes on as a non-streaming call (`max_tokens` at most 64,000, 300 s for the whole call), as Claude Code does for every call; a one-piece call that times out goes back to streaming and is not retried in one piece, and a one-piece body that is not JSON fails at once; a summarize call that is too long drops only as many old rounds as the error says it is over. `AnthropicProvider.count_tokens` has a 10 s timeout and stops asking for a model whose count the endpoint refused. - Review fixes: MCP prompt and @-resource text keeps its 64 KiB cap; `WebFetch` treats a redirect off a preapproved documentation path as leaving the host; only a task's first approved plan is named; a shell read of a file a `Read` deny rule covers brings no instruction files; the post-compaction restore resolves a command's relative path from where it ran; a failed or timed-out `git status` shows `(not captured)`; the date-change note never goes back; the git snapshot at a compaction is bounded to 10 s and falls back to the stored one; the changed-file check skips files over 256 KB and shows at most 16,384 characters of changed lines per check; container `stat_many` works with BusyBox `stat`; a file starting `FF FE` that is not UTF-16LE is not text; a parked `AskUserQuestion` keeps its answer when a later run stops offering the tool; `StructuredOutput`'s retry cap counts only refused calls since the prompt or the last accepted answer, and the tool is offered even when an agent's `tools:` list leaves it out; workflow children started before the upgrade that answer in JSON text finish normally; a task or skill activation recorded under a plugin agent's or skill's bare name still resolves when one plugin has that name. - **Breaking** (besides the structured-output change above): - A Claude Code plugin's skills, agents and commands are named `:...`; the model, the `Skill` tool, `subagent_type` and typed `/` accept only the full name (a recorded task, skill activation or settings `agent` key with a bare name still resolves when exactly one plugin has that name). `noeta.cli.claude_plugins.plugin_settings()` returns `skills.plugin_dirs` (directory -> plugin) instead of `skills.dirs`. - A structured-output schema whose root is not `"type": "object"` is refused: `--json-schema` exits with an error, `initialize` `jsonSchema` returns an error, an HTTP `output_schema` gets 400. - Task config gains `interactive`, `permission_prompt_tool` and `client_hooks` (`CHANGEABLE_CONFIG`); the terminal UI and ACP remove them when they reopen a task; an HTTP config signal may remove `client_hooks` but not set it (403). - For a task whose headless client registered hook callbacks, a process whose client has not registered the same callbacks denies a call a recorded `PreToolUse` callback matches, and denies (does not ask about) a call a recorded `PermissionRequest` callback matches; they used to run unjudged. Child tasks carry the record; a reconnected client with the same callbacks answers for them, and the terminal UI or ACP reopening the task clears the whole open tree. A `-p` run that exits leaves the record, so those calls are denied until a front end reopens the task. - `offer_tools` skips an agent's allow and deny lists for tools flagged `past_agent_lists` (`StructuredOutput`). - The summarize call's own 64,000-token second attempt and `compaction.summary.RETRY_MAX_TOKENS` are removed (the provider's non-streaming fallback replaces them); `Provider` gains the optional `complete_whole`, `ExecEnv` the optional `stat_many`. - **An empty reply no longer fails the turn; the model is asked to go on, as in Claude Code.** A reply that ends (`end_turn`) with no text and no tool call (no content, empty text, or reasoning only) is still not recorded; the next request carries Claude Code's one-line nudge as plain text in the user's turn (the `continue` entry, `reason: "empty_reply"`), once per step: again after a tool round, a new prompt or a notice. An empty reply after the nudge ends the turn quietly: no `llm_empty_response`, the answer is empty, `-p` reports `success` and exits 0, and Stop / SubagentStop hooks run without `last_assistant_message` (the log gets `turn_done` with `reason: "empty_reply"`). Other stop reasons with an empty reply still fail with `llm_empty_response`. Three Terminal-Bench 4.0 trials on DeepSeek V4.1 Flash had ended mid-task on one empty reply. - **A dropped reply counts as a model call.** An empty reply, and a reply cut at the output cap while it held only reasoning, are not recorded, but their tokens and cost now count in `/cost`, the headless `result` (`usage`, `total_cost_usd`, `num_turns`) and the per-turn token count (the `continue` / `turn_done` entry carries the reply's `model` and `usage`); and a `continue` no longer counts toward the "model call kept being interrupted" limit. An older worker reading these entries ignores the new fields: it keeps the earlier answer of a turn that ended on an empty reply and runs no Stop hook for it. - **Commands inherit noeta's environment, as in Claude Code.** `Bash`, background jobs, command hooks, stdio MCP servers and MCP `headersHelper`s started on this machine get the whole process environment except noeta's own credentials (`NOETA_API_KEY`, `NOETA_STORE`, `NOETA_SERVER_TOKEN`, `NOETA_MCP_CLIENT_SECRET`, `NOETA_WEB_SEARCH_API_KEY`, every variable an `{env:NAME}` in `config.toml`'s plugin, channel and agent tables or noeta's own agent files names, and libpq's `PGPASSWORD` / `PGPASSFILE` / `PGSERVICE` / `PGSERVICEFILE` once the store is Postgres); they used to get only `PATH`, `HOME`, the locale and a few Python variables, so toolchain settings (`CARGO_HOME`, `RUSTUP_HOME`, `JAVA_HOME`, `GOPATH`, …) were lost. Other secrets in a worker's environment (cloud keys, `ANTHROPIC_API_KEY`) now reach commands too: start the worker with a clean environment that holds only what noeta and its tasks need (the sandbox's `credentials.envVars` hides a variable from sandboxed `Bash` commands only). `LocalExecEnv(inherit_env=)` is gone; `noeta.toolkit.local_env.scrub_env` / `ENV_ALLOWLIST` are now `child_env` / `OWN_SECRETS`. - **`Bash`, background jobs and `Monitor` commands get `GIT_EDITOR=true`**, as in Claude Code: it replaces an inherited `GIT_EDITOR`, so `git commit` / `rebase` never wait for an editor; a host's `ShellEnv` variables still override it. - **The skill listing's budget is Claude Code's: 1 % of the window in characters, no ceiling.** It was 1 % in estimated tokens capped at 4,096; at a 1M window that listed about half of what Claude Code lists in full. On a 200K model counted at 3 characters a token the budget is now 6,000 characters (about 8,000 before), so the listing shortens a little earlier there; CJK descriptions count a character per character, as in Claude Code. `Skills(budget_tokens=)` is now `Skills(budget_chars=)`. - **A later turn gets the git snapshot again when the branch or HEAD moved since the previous turn ended** (someone switched branch or committed in between; the model's own commits do not count). noeta's own trigger: Claude Code re-announces a changed session context when a session resumes, and a durable task has no resume moment. `Environment(git_refresh=False)` keeps the start snapshot only. - **A pasted image is also saved as a file and named in one line, `[Image source: ]`**, so the model can copy it into the project (Claude Code's pasted-image paths): `/image-cache//` on a local exec env (a sandboxed command may read it), `/noeta-images-//` inside any other; private (0700 / 0600), written again if gone at a later turn, removed when the root task ends or after a week unused. - **The memory recall selector no longer fails silently**: a reply that is not the schema's JSON still picks the listed file names in it, a provider that refuses `thinking: disabled` (HTTP 400) is asked again without it (remembered per model), and any other failure logs one warning per reason. - **`Extension.on_context_overflow(view, request)` gets the request that overflowed.** An override written as `(view)` still runs. Compaction no longer keeps each task's last request in memory for it. - **`WebFetch`'s digest goes out like every other call beside the step**, through `ToolContext.complete`: without the headers a hook put on the main request, which it used to copy from process memory and lost when another worker ran the call. A host that needs them on every call sets them on the provider, or passes `digest=`. - **Live output has a new kind, `Delta(kind="tool")`** (a running tool's progress), and readers now skip a kind they do not know. A process from before this release does not: an older `RemoteClient` watching a newer server loses its watch on the first progress, and on a shared Postgres an older process drops the whole notification batch holding one. In a rolling upgrade, upgrade the front ends and `noeta serve` before the workers; an SDK host on `app.deltas` should ignore kinds it does not know. - **A settings file's hooks set no `env`**: that field is noeta's, for a Claude Code plugin's options. - **`Agent.inherit_model`, `permission_mode` and `isolation` are gone; `ToolContext.extras` too.** Only the sub-agent plugin acted on the three: it reads them from `Agent.extra` as an agent file spells them (`model: inherit`, `permissionMode`, `isolation`); an SDK host building an `Agent` in code sets them there. `extras` was never used. - **Skills, memory recall, instruction files and list changes follow Claude Code 2.1.312.** A skill's listing line is capped at 1,536 characters (not 384 estimated tokens); a skill whose description changed is listed again; a loaded skill is its base-directory line and body (no "Activated skill" header or description); a `/` named mid-message gets a note; skill directories below the working directory join when a file there is touched; the `priority:` skill key is gone. Recall shows a memory's first 200 lines / 4,096 bytes, notes a memory older than a day, skips one-word messages, sub-agents and files already read, and its selector is sparing with `[user]` / `[project]` memories. An `@import` of a non-text file is skipped; a long nested instruction file is cut to its start; a changed instruction file is shown again with the next message. Deferred tools and agent types that join or leave mid-task are told, MCP servers' `instructions` reach the model, and `ToolSearch`'s `+word` also matches the description. Records written for these live in the task log. - **A plugin file missing a package says how to add it.** Plugins install no dependencies; one that imports a module noeta's environment lacks is listed as broken (`noeta plugins`, `/reload-plugins`) with the `uv tool install` / `pip install` line that adds it, or the extra it belongs to. - **A stopped turn leaves background jobs running; `TaskStop` sends no end notice.** As in Claude Code, stopping the root's turn (Esc, `cancel` with `turn_only`) no longer kills the tree's `run_in_background` commands and monitors: they keep running and report their end as usual, and still die when the root ends or is cancelled. A job stopped with `TaskStop` ends quietly; one killed for another reason still notifies. - **A plugin's markdown command runs in the task, as a typed skill does.** Typing `/:` sends Claude Code's command tags, and the task adds the body: its `` !`command` `` lines run through the permission checks (one that would ask stops it, with a note), and its `allowed-tools` (frontmatter, or a manifest entry's `allowedTools`) are pre-approved from then on. `noeta.plugins.skills.inline_commands` and `allowed_tools` moved to `noeta.toolkit`. - **Deferred tool loading is the `tool_search` plugin; the kernel holds no capability.** `ToolSearch`, its search, the not-loaded error and `DeferredToolPlaceholder` moved from `noeta.core.deferred` (removed) to `noeta/plugins/tool_search`; the requests the model gets are unchanged. For plugin authors: a tool still defers with `deferred = True` and is still announced through `TaskView.deferred_tool_names`; `TaskState.loaded_tools` is gone (the loaded names are the `ToolReference`s in the task's history and open batch). Every agent built from agent files runs `tool_search` whether it lists it or not (`[plugins.tool_search]` `enabled = false` turns it off); an `Agent` built in code that offers deferred tools adds `ToolSearch()` from `noeta.plugins.tool_search` itself, or its deferred tools cannot be loaded. A snapshot written before this change is not used (the fold changed): its task folds from the log once. - **Stop every worker before upgrading; workers of different versions must not share a store.** This release writes log entries an older worker cannot decode (messages with `role="system"`, origins `inline` and `agent`, `ToolReference` blocks), so an older worker that picks up a task a newer one touched fails on it again and again. Logs written by older versions still load. - **Stores are at schema version 4 and refuse a newer store.** Holds moved to `tree_holds`, and `snapshots` (keyed by task and fold version) were added. The first process of this version moves a version 1 store (sqlite or Postgres) forward in place, posting `hold_lost` for the old `holds` rows and then dropping that table (a version 2 store from a development build gets its snapshot rows re-keyed, and a version 3 one loses the `tasks.tenant` column and `tenant_usage` table of the tenants that were removed before release); a store at a later version than the code knows is refused with `StoreUnavailable` before anything is written to it (sqlite never checked before). Upgrading: stop every worker first; an older sqlite binary does not refuse the upgraded store: one left running fails, one started again writes a new `holds` table, which the next open drops again (a tree told before is not told twice). - **noeta reads only its own places, with Claude Code's mechanisms.** `~/.claude/`, `.claude/`, `CLAUDE.md`, `CLAUDE.local.md`, `.mcp.json` and `CLAUDE_CONFIG_DIR` are no longer read, so the two tools never pick up each other's hooks, env or instructions. Instructions are `NOETA.md` in Claude Code's layering: the managed `NOETA.md` and `.noeta/rules` (`/etc/noeta`; macOS `/Library/Application Support/Noeta`, Windows `C:\Program Files\Noeta`), the user's `~/.noeta/NOETA.md` and `~/.noeta/rules/`, then per directory `NOETA.md`, `.noeta/NOETA.md`, `.noeta/rules/` and `NOETA.local.md` (`AGENTS.md` where a directory has neither project file). Settings are `~/.noeta/settings.json`, a trusted project's `.noeta/settings.json` and `.noeta/settings.local.json`, and the managed `managed-settings.json` (same directory), in Claude Code's format and precedence (`noeta.cli.claude_settings` -> `noeta.cli.settings_files`, `ClaudeSettings` -> `FileSettings`). Skills come from `.noeta/skills`, `.agents/skills` and `~/.noeta/skills`; agent files from `~/.noeta/agents` and `.noeta/agents`; project MCP servers from `.noeta/mcp.json` (same format and consent keys); isolated agents' worktrees go under `.noeta/worktrees/`. Claude Code's environment knobs carry noeta's prefix: `NOETA_AUTO_COMPACT_WINDOW`, `NOETA_AUTOCOMPACT_PCT_OVERRIDE`, `NOETA_MCP_TIMEOUT`, `NOETA_MCP_TOOL_TIMEOUT`, `NOETA_MAX_MCP_OUTPUT_TOKENS`. Texts the model sees name `NOETA.md` and `.noeta/` where they mean noeta's files (`parity/deviations.md`). - **Compaction works as Claude Code's.** It fires at Claude Code's threshold (the window, less `min(output ceiling, 20K)`, less 13K; `auto_compact_window` / `auto_compact_percent` or `NOETA_AUTO_COMPACT_WINDOW` / `NOETA_AUTOCOMPACT_PCT_OVERRIDE` lower it), checked before every request. The summary is asked of the main request itself, with the instruction (nine sections, `` then ``) appended to its last user message; a failed call is retried once with a 64K output ceiling. When the context was under the threshold before the newest tool results, that reply and its results stay after the summary verbatim. After it come the project instructions (ahead of the summary), the summary with the tools used before it and the path of the full transcript (`/projects//.jsonl`), the kept messages, then the recently read files as Read calls, the invoked skills ("do not run again"), the listings and the environment. Old tool results are no longer cleared, `RecallHistory` and the compaction reminders are gone, and auto-compaction stops after four rapid refills in a row. `Compacted` gains `lead` and `after`; `Extension.lead_resident` puts content ahead of the summary. - **`/compact [instructions]`** summarizes the conversation on demand in the TUI, ACP and HTTP (`POST /tasks/{id}/messages` with `/compact`), without a model turn after it. - **Memory is Claude Code's auto memory.** The `memory_*` tools, the store, recall by keywords and the consolidation agent are gone. Memory is a directory of markdown files under `~/.noeta/projects//memory/`, written with `Write` / `Edit` as the system prompt's memory section describes (types `user`, `feedback`, `project`, `reference`); `MEMORY.md` is loaded at the start (200 lines / 25,000 bytes) and after a compaction. With `recall_model`, each user message attaches up to five memories that model picks. The plugin's arguments are now `home`, `directory`, `recall_model`, `rehang`. - **A sandbox for local commands, with Claude Code's `sandbox` settings.** With `sandbox.enabled` in a settings file, `Bash` (foreground, background and `Monitor`) runs each command confined: bubblewrap on Linux (own network, PID, IPC namespaces; a seccomp filter refusing Unix sockets and new user namespaces unless `allowAllUnixSockets` / `allowNestedUserNamespaces`), Seatbelt on macOS. Commands write only in the working directory, `filesystem.allowWrite` and a private `$TMPDIR`. Never in `denyWrite` or what runs or configures code outside the sandbox later: the permissions plugin's protected paths (`noeta.toolkit.protected_paths`: the whole `.noeta` directory, `.vscode`, `.husky`, `.envrc`, `.pre-commit-config.yaml`, shell and package-manager settings, ...), `.git/hooks`, `.git/config`, `.git/config.worktree`, each submodule's config and hooks under `.git/modules`, and the hooks directory git uses (`core.hooksPath`). What is executed on the next ordinary command without a confirmation step cannot be created when missing either (an empty read-only placeholder covers it while the command runs): `.noeta`, `.git`, git's hooks directory and config, and the settings a package manager or build tool runs code from (`.npmrc`, `.yarnrc`, `.yarnrc.yml`, `.yarn`, `.pnpmfile.cjs`, `.pnp.cjs`, `.pnp.loader.mjs`, `bunfig.toml`, `.bunfig.toml`, `.cargo`, `.mvn`, `.bazelrc`, `.bazelversion`, `.bazeliskrc`; lefthook's configs once lefthook is in use) — 16 placeholders in an empty directory. Git does not list them when the working directory is a repository's top (the command's `.git/info/exclude` names the files). The other protected names are read-only only when present, since each runs only after a separate step of the person's (`direnv allow`, an editor trusting the workspace). No directory above a protected path can be renamed away. Git keeps working (status, add, commit, checkout, submodule status), but **in a directory that is not a repository `git init` fails inside the sandbox** (no `.git` can be created), and **nothing under `.noeta/` — skills and commands included — can be written from the sandbox** (the permissions plugin still lets `Edit`/`Write` change `.noeta/skills` and `.noeta/commands`). Commands read everything but `denyRead`, credential stores and noeta's home, and reach only `network.allowedDomains` through a filtering HTTP/SOCKS proxy, which connects to the address it checked and refuses one that is loopback, link-local, multicast, a cloud metadata service or this machine's own unless an IP or CIDR entry allows it (as Claude Code); this machine, in any spelling of its address, needs an entry with its port even under `*`. Refused hosts follow the output in ``. A `Monitor` WebSocket, which the worker opens itself, keeps to the same rules. `Edit` allow rules, `additionalDirectories`, `Read` deny rules and `WebFetch(domain:)` allow rules feed it; `excludedCommands`, `allowUnsandboxedCommands`, `autoAllowBashIfSandboxed` (default on), `failIfUnavailable`, `credentials` (`mask` acts as `deny`), `httpProxyPort` / `socksProxyPort`, `enableWeakerNestedSandbox`, `filesystem.disabled` and `bwrapPath` are honoured, and a project's settings may tighten the sandbox but never loosen it: its `excludedCommands`, `allowWrite`, `allowRead`, `allowedDomains`, `allowUnixSockets`, the loosening switches, and its permission allow rules and `additionalDirectories` never reach the sandbox (only the user's and managed settings widen it). `dangerouslyDisableSandbox: true` runs a command outside after an approval that allow rules do not skip. `Bash`'s description gains a sandbox section while the sandbox is on. Without bwrap (or namespaces) front ends say why and commands run unconfined, or are refused with `failIfUnavailable`. API: `LocalExecEnv(sandbox=...)`, `env.sandbox`, `noeta.toolkit.sandbox`. - **`start_container` is locked down by default:** no network, a read-only root with tmpfs scratch, a reduced capability set, `no-new-privileges` and a 1024-process limit (each an argument: `network`, `read_only`, `caps`, `pids_limit`, plus `memory` / `cpus`). Its `sandbox=` maps `network.allowedDomains` onto an internal Docker network whose only way out is noeta's filtering proxy (`ensure_container_proxy` serves it again after a restart). - **MCP transports as Claude Code's:** `sse` (legacy HTTP+SSE) and `ws` (WebSocket) join `stdio` and `http` (`type: "streamable-http"` reads as `http`); `alwaysLoad` and `bareElicitationCapability` are read from settings. Requests are multiplexed: a timed-out call is cancelled and no longer breaks a stdio connection. - **MCP OAuth 2.1** for `http` / `sse` servers: resource and authorization-server discovery, dynamic client registration or `oauth.clientId`, PKCE, `oauth.callbackPort` / `NOETA_MCP_OAUTH_CALLBACK_PORT`, `oauth.authServerMetadataUrl`, `oauth.scopes`; tokens in `$NOETA_HOME/mcp-auth` (0600) and refreshed. A server that needs a sign-in offers `mcp____authenticate` / `mcp____complete_authentication` in place of its tools. `headersHelper` runs a command for the headers when a connection opens (`NOETA_MCP_SERVER_NAME` / `NOETA_MCP_SERVER_URL` set). - **MCP servers can ask the user** (elicitation, turned into the question form; the call waits for the answer), read the task's workspace from `roots/list`, and refresh their tools with `list_changed`. - **`noeta mcp add|add-json|list|get|remove|login|logout`** with Claude Code's flags, writing `~/.noeta/settings.json` (user), `.noeta/mcp.json` (project) and `.noeta/settings.local.json` (local). `McpPool(http=...)` routes remote MCP connections and sign-ins through the host's `httpx.AsyncClient`. - **`noeta -p` is Claude Code's headless mode.** `-p` is a flag and the prompt positional (or stdin); `--output-format text|json|stream-json`, `--input-format stream-json`, `--include-partial-messages`, `--replay-user-messages`, `--max-turns`, `--max-budget-usd`, `--json-schema`, `--allowedTools` / `--disallowedTools` / `--tools` / `--add-dir`, `--permission-prompt-tool stdio`, `-c` / `-r` / `--fork-session` / `--session-id`, `-v`; flags an Agent SDK passes that noeta does not use are accepted and named on stderr. Without `--permission-prompt-tool stdio` anything that would ask is refused and the turn goes on (it used to exit 2). `--json` is gone; use `--output-format stream-json --verbose`. - **`create_app` requires a bearer token**, as `noeta serve` does: without `token=` it raises unless the host passes `auth=False`. - **The main loop sends what Claude Code 2.1.312 sends an API-key user.** The system prompt is two cached blocks: an identity line (`Agent.identity`, agent-file `identity:`, prompt name `identity/`) and the main prompt, rewritten to Claude Code's rules and section order in our own words (noeta-only rules dropped, `parity/rules/main-prompt.md`). The project instructions, a git snapshot and a commit/PR attribution note ride in the first user turn; the environment (with the model's name and knowledge cutoff), the skill listing and the date follow it as a mid-conversation system turn (`Message.role="system"`, `Reminder.role` and `Reminder.order`; the Anthropic adapter joins adjacent ones, the OpenAI adapters send them as framed system messages). A `` budget note follows every prompt and tool result, and five tool-only steps in a row earn one nudge to update the user (`quiet_nudge` plugin). The Anthropic adapter sends Claude Code's beta set, adaptive thinking with summarized updates, `context_management` keeping old thinking, effort `medium` by default, `metadata.user_id` (a random device id kept in `~/.noeta/device_id`, the task tree's id), three cache marks (no tool mark; one message mark, on the last message), single-text turns as bare strings, and a message sent mid-turn inside the tool results' turn. `AnthropicProvider(system_turns=False)` is for an endpoint without system turns. `ContextPart.cache` is gone (the prompt is one cached block). Accepted differences are listed in `parity/deviations.md`. - **Packages say what they hold** (old paths are gone, no aliases): `noeta.core.shell` / `tool_helpers` / `helpers` -> `noeta.toolkit.shell` / `tools` / `context` (code plugins share; the kernel never used it); `noeta.core.store.base` -> `noeta.core.store`; `noeta.core.store.sqlite` -> `noeta.backends.stores.sqlite`; `noeta.backends.postgres` -> `noeta.backends.stores.postgres`; `noeta.backends.execenv` -> `noeta.backends.execenvs`; `noeta.apps.local` / `remote` -> `noeta.host.*`, `noeta.apps.server` -> `noeta.host.http`, `noeta.apps.acp` -> `noeta.host.acp` (the ways in sit together); `noeta.apps` -> `noeta.cli` (the command is `noeta.cli.main`), with `from_settings` in `noeta.cli.config`; `noeta.presets` -> `noeta.agents` (agent files in `noeta/agents/files/`); `noeta.core.agent_files` -> `noeta.agents.agent_files`. `noeta` exports `LocalApp` and `RemoteClient`. The store contract tests ship as `noeta.testing.store_contract`. `scripts/lint-layout.py` (in `make check`) keeps code where CONTEXT.md's "Where code goes" puts it. - **1.0 is a rewrite, one package `noeta`.** Tasks are append-only event logs with one writer (conditional appends, claims in the store), an inbox as the only way in, and one `Extension` interface for every capability. Includes the `noeta` command (terminal UI, `-p`, `acp`, `worker`, `serve`). Nothing from 0.x is compatible: APIs, stores and plugins. - **Plugins are set from one table.** `config.toml` takes `[plugins.]` (arguments, `enabled = false`) and `[agents.] add_plugins`; `default_agents` takes `plugins=` and `add=` instead of one keyword per built-in plugin. - **MCP servers from settings.** The terminal front ends start the `mcpServers` of `~/.noeta/settings.json` (Claude Code's format) and the `.noeta/mcp.json` servers it consents to. - **Plugin slash commands** show in the TUI's `/help` and as the editor's commands over ACP; MCP prompts are commands. - **Background jobs hold their task tree.** While a job runs, no other worker claims a task of its tree (`Store.hold` / `unhold` / `reap_holds`, a `tree_holds` table, `ToolContext.hold`), so the next turn can read and stop it with several workers on one store. A hold carries an epoch; every worker reaps a silent worker's holds and posts `External("hold_lost")` to the tree's tasks in the same transaction, and the model is told the job was lost (Claude Code's `` shape). A job runs beside a guard that kills it when its worker dies or freezes; its end is posted as `External("shell_job_ended")` before the hold is released and settled once by the task's runner. A worker stopping on purpose (closing the TUI) posts `hold_lost` without waking the task (`Post.wake`): it hears with its next message. Third-party stores implement `hold`, `unhold` and `reap_holds` (a store an earlier development build opened has its old `holds` table dropped; upgrading it first posts `hold_lost`, with epoch 0, to the tasks of each tree held there, and the tree's running jobs recorded without an epoch are told lost). A hold is refused while another worker runs a task of the tree, and on Postgres a claim and a hold of one tree serialize, so a held tree's tasks run only on its holder; a background command refused for either reason says so and points the model to the foreground or a later try. A hold also takes back another worker's lapsed claim of the tree (a new epoch, back to `ready`), so a worker that stalled past its lease cannot renew it or write once the tree is held elsewhere. - **Plugins as files.** `~/.noeta/plugins/*.py` (or a directory) is a plugin without packaging; a project's `.noeta/plugins/` loads once trusted (`noeta trust`). `noeta plugins` lists where each plugin comes from. - **Any plugin can ask the person and notify them.** `noeta.ask` / `noeta.read_answer` (`noeta.core.questions`, moved from `ask_user`) and `notify` on `TaskView` / `ToolContext`; front ends import no plugin. `Mcp` takes servers in Claude Code's `mcpServers` shape. - **Kernel and toolkit surfaces moved to where they are used** (no aliases): the permission-mode vocabulary is `noeta.toolkit.permission_modes`; approval cards, the pending projection and the typed-answer helpers are `noeta.host.pending` / `noeta.host.questions`; env scrubbing and Claude Code's command tags are `noeta.toolkit.local_env` / `noeta.toolkit.commands`; `noeta.toolkit.context` is split into `tokens`, `paths` and `memory_index`; shell hardening (`HardeningRule`, was `AllowRule`) is in the shell plugin; `toolkit.simulated_state` is `TaskView.folded()`. The core's own records have typed payloads (`noeta.core.records`, `records.event` / `records.decode`; bytes unchanged). `core.client.ClientAPI` names the client API that `Client` and `RemoteClient` implement. Renamed for what they do: `Agent.max_steps_per_turn` -> `step_budget`, `ModelSpec.family` -> `vendor` (a models file keeps `"family"`), the module `noeta.plugins.structured` -> `noeta.plugins.structured_output` (its registry name). - **Store interface.** `append` returns `Appended(seq, at)` (the stored time); a claim clears the row's `waiting_on`; giving back a claim or a hold reports a `released` change, only when a row changed; optional `snapshot(task_id, version=None)` / `put_snapshot`; `Post.wake`. Creating a task with a known id is idempotent under a race (store contract test). - **The plugin set is pinned per task.** A task records each plugin's name, source (package version or file hash) and a hash of its arguments when it first starts and when a pickup finds them changed; a worker missing one never runs the task without it: it lets the lease run out for a worker that has the plugin (a rolling install), and ends the task failed with `plugin_missing` after three such claims in a row with nothing new in the log. - **`Agent` fields for what a sub-agent definition asks**: `inherit_model`, `permission_mode`, `isolation` (were read from `Agent.extra`). An agent built in code runs its sub-agents on its own model unless it sets `inherit_model`. - `Runtime.output_ceiling(model)` gives a request's default `max_tokens` (default: the catalog); plugins read the working directory through `TaskView.cwd` (config, else the exec env), so a task created without a `cwd` runs its hooks and finds its project MCP servers where its commands run; `UsageRank` reads through a client; `ToolContext.post` takes a `dedupe_key` and reaches any task; permissions records `probed` / `compacted` and a front end's `resumed` signal is SessionStart `resume` (so a task moving between workers sends the same request). - **The permissions plugin publishes its modes** (a `modes` record without `bypassPermissions` when that is disabled); the TUI and ACP offer what it lists. `noeta serve` turns on the plugin's own `bypassPermissions` refusal. - **Built-in tools answer as Claude Code 2.1.312's tools do.** - `Read` without `limit` returns the whole file (within 256 KB and 25,000 tokens) instead of its first 2,000 lines, and keeps long lines whole instead of cutting them at 2,000 characters. A whole-file read past the token cap returns the lines that fit and a note with the next `offset` instead of being refused. Images and PDFs are recognised by extension (content is sniffed only without one); empty, headerless or over-100 MB PDFs, known binary types, devices and `/proc/` files are refused with a reason. An image comes back as the image alone; a UTF-8 byte-order mark is hidden; a missing file names the working directory and a likely intended path; an empty file or an offset past the end is a warning. The "unchanged since last read" answer needs the same `offset` / `limit` and covers notebooks. - `Edit` matches straight quotes against curly ones and `\uXXXX` escapes against the characters they stand for, keeping the file's own spelling; deleting text takes the following line break along; an empty `old_string` creates the file (or fills an empty one). It refuses notebooks (use `NotebookEdit`) and files over 1 GB, its not-found error repeats the string it looked for, and a file changed on disk since the read is still edited when the write is allowed outright. `Edit` and `Write` skip the read-first check for a write no one had to approve. `Write` keeps a file's UTF-8 byte-order mark and, given a directory, asks for the file name. - `NotebookEdit` takes `cell-` for any cell, answers with the cell's new source, and gives an inserted cell an id only in an nbformat 4.5+ notebook. - `Bash`: exit status 1 of `grep`, `rg`, `find`, `diff` and `test` is an answer, not an error; a command still running at its timeout moves to the background (with its job id and output file) instead of being killed; output that is one `data:image` URI is an image. - `WebFetch` refuses URLs over 2,000 characters or with credentials, waits 60 s and accepts 10 MB (was 10 s, 5 MB); an HTTP error status, an offset past the end and a cross-host redirect (naming the status and the call to make) are ordinary results; `www.` redirects are followed; a PDF or other binary body is saved to a file the model can `Read`. `WebSearch` returns the query, the hits as JSON and a reminder to cite, and refuses `allowed_domains` together with `blocked_domains`. - An MCP result over the token budget is saved and pointed to, not cut inline; `ReadMcpResourceTool` saves a binary resource and gives its path. - `Agent` matches `subagent_type` ignoring case, spaces, dashes and underscores when exactly one type fits, and a bad `Agent` call fails alone while the reply's other calls run. `SendMessage` refuses `to: "*"`; `TaskStop` answers with Claude Code's result object. - `AskUserQuestion` no longer refuses long or empty texts, refuses two questions with the same text, steers a one-option question, and tells the model to read typed answers as written. `Skill` accepts `/name`, follows a skill called again with its new arguments, names a user-only skill as such and suggests a near match for an unknown name. `ToolSearch` ranks keyword queries as Claude Code does and `select:` finds an MCP tool by its own name. `ReportFindings` refuses values outside its schema; `Workflow` runs at least two agents at once and lists the saved workflows for an unknown name; `ExitPlanMode` outside plan mode says what to do next; `EnterWorktree` refuses `.` and `.git` segments. - **More tool messages in noeta's own words.** The errors and notes the recordings never show still repeated Claude Code's sentences; they now say the same in noeta's words, with the same numbers: `Edit` on several matches, the read-before-write refusal, `Read`'s size and token ceilings, the PDF page limits, a missing `pdftoppm`, a password-protected or unrenderable PDF, a notebook's oversized outputs, the note about a file changed since it was read, and `Bash`'s refusal of a long leading `sleep`. ### Store contract Third-party `Store` implementations must add these; `noeta.testing.store_contract` checks them. - **Refuse to end a task over an unread inbox.** An append that ends a task while its inbox holds items past the read seq raises `InboxUnread` (a `Conflict`), checked under the task's row lock. The runner reads the input and decides again, so a sub-agent can no longer end without reading a `hold_lost` and drop a background job silently. - **`Post.forward_if_ended`.** When set and the target task has ended, the item goes to the root's inbox instead (same dedupe key), decided under the target's row lock; `post` returns the seq of the inbox it went to. Background jobs' ends and monitor events use it (`ToolContext.post(forward_if_ended=True)`). - **`Claim.queue`.** A claim (and its renewal) names the queue of the task claimed; a store that leaves it out puts every child on `default`, as before. - **`Store.hold(queues=)`.** Taking a hold with the holder's queues is refused (`None`) while an unended task of the tree is on a queue not among them, decided in the hold's transaction; `Runtime.hold` passes its queues. - **`append(creates=)` refuses a child its tree's holder cannot claim.** A hold keeps the `queues` it was taken with; an append creating a child of a held tree on a queue not among them raises `UnservedQueue` and writes nothing, decided against a hold being taken at the same time. The SQL stores add `tree_holds.queues` in place (an older hold's is empty and not checked). ### Added - **`TaskView.spill_dir`**: the spill root, whose saved outputs the permissions plugin lets `Read` open without asking, as it does the task's plan file. A host sweeps its spill root (`noeta.host.spill_sweep`): an ended task's directory not written for 30 days is removed. - **A Python plugin directory may ship `skills/`, `commands/` and `agents/`** in Claude Code's format next to its `__init__.py`, as pi's and dsh's plugins add resources; they load as a Claude Code plugin's do (commands as `/:`). Before, a directory was either Python or a Claude Code plugin. - **Claude Code plugins' `userConfig` and `settings.json`.** `noeta plugin add` asks for a plugin's `userConfig` values (or takes `--config KEY=VALUE`; `noeta plugin configure` sets them again), keeps them in `~/.noeta/settings.json` `pluginConfigs` with sensitive ones in the OS keyring (a machine whose keyring cannot keep it is refused before anything is installed, with the `{env:NAME}` way instead), and substitutes `${user_config.KEY}` in MCP servers, hook `args`, commands and agent prompts (not yet in skill bodies); every hook of the plugin gets each value as `CLAUDE_PLUGIN_OPTION_`. A secret a machine cannot read (its keyring lacks it, a variable is not set) stops only what uses it, with a notice naming the plugin: hooks get no variable for it, an MCP server or hook that names it is left out, and a `PreToolUse`, `PermissionRequest` or `UserPromptSubmit` hook that names it refuses. A plugin's own files may name only its own keyring secrets. A plugin's `settings.json` `agent` (and the settings files' own `agent`) picks the agent front ends start with, and the TUI honours `--agent`. - **A running tool shows its progress** (`ToolContext.progress(text)`), as Claude Code's `tool_progress`: never recorded nor sent to the model, it streams with the model's live output (across machines on Postgres). A foreground `Bash` command shows its latest output after 2 seconds, then every second: the TUI shows its last lines under the call, ACP sends a `tool_call_update`, and `-p` / stream-json a `tool_progress` message (always, not only with `--include-partial-messages`). - **The names a plugin author uses come from `noeta`**: `Allow`, `Deny`, `Ask`, `Rewrite`, `ToolChanges`, `ToolUse`, `Recorded` and `TaskState` join `Extension`, `Tool` and the rest, so a plugin file needs one import line. - **`InstructionsLoaded` hooks fire as in Claude Code**: once per instruction file shown (at the start, nested, an `@import`, a `paths:` match, after a compaction, and a changed file shown again), with `file_path`, `memory_type`, `load_reason`, `globs`, `trigger_file_path` and `parent_file_path`; matched on `load_reason`. - **`ToolContext.complete`: a tool can call the model** through the task's providers (retried, not recorded as a step), as hooks can with `TaskView.complete`. - **`register_models` accepts a provider family beyond `anthropic` / `openai`** (also a models file's `family`), so a third-party provider package can declare its own; only those two carry noeta's conventions, and a near-miss like `antropic` is still refused. - **`--plugin-dir` loads a plugin for one run.** `noeta --plugin-dir ./x.py` (TUI and `-p`, repeatable) loads a plugin file, a plugin directory, a Claude Code plugin or a directory of them for this process only, as Claude Code's flag does; it was accepted and ignored. A plugin file that joins no agent (no `add_to`, no `add_plugins`) is named in a notice. - **`ToolContext.serves_queue(queue)`**: whether a child staged on `queue` could run now (the runner drops one that could not, with the call's result); `None` takes every queue. - **`Extension.tool_changes(view, offered)`.** A plugin changes the task's tool list from the tools the other plugins offer, without asking them itself: `ToolChanges` adds tools, withholds offered ones from the request (a call to one gets the given error before any gate) and adds `ToolEntry`s (request entries that are no tool, placed before a named tool). Every extension gets the same `offered`, so the outcome does not depend on plugin order. - **The rest of Claude Code's deferred tools and frontmatter.** `EnterPlanMode` (asks the person outside auto mode), `EnterWorktree` / `ExitWorktree` (the shell moves into a worktree under `.noeta/worktrees/`), `ReadMcpResourceDirTool`; `WebSearch` takes `allowed_domains`, `blocked_domains` and a required `mode` (its `count` is gone). Skills read `when_to_use`, named `arguments`, `${NOETA_SKILL_DIR}`, `` !`cmd` ``, `paths` and `context: fork`, and `allowed-tools` pre-approves. Agent files apply `skills`, `mcpServers`, `hooks`, `memory`, `autoCompactWindow` and `initialPrompt`, and warn about unknown fields. Hooks gain the `agent` and `mcp_tool` types and the PostToolBatch, StopFailure, PostModelSwitch and CwdChanged events. `noeta -p` applies `--settings`, `--setting-sources`, `--mcp-config`, `--strict-mcp-config` and `--agents`, and a `can_use_tool` answer's `updatedInput` for every tool. MCP and Agent results reach Anthropic models as a block list (`ToolOutput.block_list`); a reply with unparseable tool-call arguments is retried once. - **Front ends run on one API, in process or against a server** (`noeta.host.app.AppAPI`): ACP and stream-json no longer reach into a local `Runtime`; `noeta acp --server URL` drives a `noeta serve` server's tasks. New `POST /commands/run`. - **Live output across machines**: on Postgres a worker publishes its streamed text (`PostgresStore.publish_deltas`, channel `_deltas`, best effort) and a server's `/watch` hears it (`on_delta`), so `noeta serve --no-worker` streams tasks that `noeta worker` processes elsewhere run. On sqlite live output stays in its process. - **Claude Code's headless protocol** (`noeta.host.stream_json`): the stream-json messages (`system`/`init`, `assistant`, tool-result `user`, `stream_event`, `result` with usage, cost, durations, turns, permission denials and `structured_output`; the root task's id is the `session_id`) and control requests (`initialize`, `interrupt`, `set_permission_mode`, `set_model`, `rewind_files`, `get_context_usage`, `get_session_cost`, `can_use_tool` to the client), so a program written for Claude Code's Agent SDK drives `noeta` (checked with the Python SDK). The HTTP server speaks it too: `WS /stream-json` and one-way `POST /stream-json`. - **Rewind with file checkpoints.** The `checkpoints` plugin (main and general-purpose agents) records a file's content before `Edit`, `Write` or `NotebookEdit` changes it; `noeta.host.rewind` goes back to before a prompt — a new task continuing the log up to it, the files restored, or both — from the TUI (`/rewind`, Esc Esc), ACP, HTTP (`/tasks/{id}/rewind`) and the headless `rewind_files`. - **TUI commands with mechanisms**: `/model` (switches the running task's model), `/context`, `/cost`, `/memory`, `/config`, `/rewind`, `/clear`, `/resume `; `@path` attaches a file, directory or image; a pasted image path or Ctrl+V attaches an image. ACP answers `/model`, `/context`, `/cost`, `/rewind`. - **HTTP**: `/commands`, `/agents`, `/models`, `/tasks/{id}/usage`, `/tasks/{id}/context`, `/tasks/{id}/rewind`, `/openapi.json`; `POST /tasks` takes `history_from` / `history_through`. `websockets` joins the `server` extra. - **`noeta serve` always asks for a bearer token**: `NOETA_SERVER_TOKEN`, else one made once in `~/.noeta/server-token` (owner-only, printed at start, read by `noeta --server`); `--no-token` drops it on a loopback address only. A request from another web origin (`--allow-origin` adds one) or with a `Host` other than a loopback name on a loopback address is refused, and no request creates or switches a task into `bypassPermissions` (`--allow-bypass-permissions`) or runs one outside the server's directory. **`noeta --server` started in a directory outside it is refused**: start the server there, or add the directory with `noeta serve --allow-root PATH` (repeatable). A relative `cwd` in a request is taken from the server's directory, not the server process's, and stored absolute (`create_app(allowed_hosts=, origins=, allow_bypass=, cwd=, roots=)`). - **Kernel**: the `config` signal changes a running task's `model`, `effort` or `max_turns` (`CONFIG_SIGNAL`); `config["max_turns"]` fails a turn after that many model calls (`max_turns`); `Client.create(history_from=, history_through=)`; `Runtime.preview` (the next request, unrecorded); `Command.local` and `run_typed` (a plugin command whose output is shown, not sent); an `Answer` deny with `"interrupt": false` gives the model an error result and lets the turn go on. - **Plugins as single files that join agents by themselves**: `Extension.add_to` (`("main",)`, `("*",)`) for a file in `~/.noeta/plugins` or a trusted project's `.noeta/plugins` (an installed package's is ignored). `/reload-plugins` (`LocalApp.reload_plugins`) re-imports plugin files and remakes the agents between turns. - **`noeta plugin add|list|remove`**: installs from `@` (a commit is required), a local path, or a Claude Code `marketplace.json` (`--marketplace`, only read, an entry installed at its sha, or at `--commit` when it pins none); nothing from the plugin runs while installing. - **Claude Code plugin directories** in noeta's plugin places load through the plugins that read their formats (skills, agents, hooks, `.mcp.json`; `commands/*.md` through the new `markdown_commands` plugin as `/:`), with `${CLAUDE_PLUGIN_ROOT}` and `${CLAUDE_PLUGIN_DATA}` filled in. - **Channels** (`noeta.core.channel.Channel`, entry-point group `noeta.channels`, `config.toml` `[channels.]`): long-running listeners run by `noeta worker` and `noeta serve`, posting idempotently through `deliver()` so replicas are safe. - **`SendMessage` `notify_when_idle`**: one notice when another open session next goes idle or ends (expires after 12 hours); `message` is optional with it. A background agent keeps its transcript in `//tasks/.output`, named by the launch result and the completion notice. `TaskView.enqueue(task_id=)` posts to another task. - **ACP**: `Edit`, `Write` and `NotebookEdit` changes are sent as diffs (old and new text); plan mode's plan file is sent as the editor's plan; model and mode are session config options, also advertised by a loaded session. - PyPI metadata in `pyproject.toml` and a `keyring` extra for `{keyring:}` secrets. - `function_tool(fn)`: a typed Python function becomes a `Tool` (exported from `noeta`). Secrets in plugin arguments as `{env:NAME}` / `{keyring:service/user}` (the optional `keyring` package), resolved only when the plugin is made. Machine policy: `/etc/noeta/config.toml` `[policy] allowed_plugins` / `blocked_plugins` win over every other setting. - **Crash-safe tool calls beyond "outcome unknown"**: `ToolContext.idempotency_key` (`:`, also sent as MCP `tools/call` `_meta`), `Tool.recover` (look the outcome up) and `Tool.retry_safe` (run again, bounded). Read, ToolSearch, WebFetch, WebSearch, CronList, ListAgents and the MCP resource tools are retry-safe. - **Fold snapshots** (`noeta.core.snapshots`, a `snapshots` table in both stores): a claim folds the task's newest snapshot plus the events after it; written every `Runtime.snapshot_every` (100) events, versioned by the code that folds (hashed when the `Runtime` is made, so code upgraded on disk under a running worker does not change it), never trusted when the version differs. One is kept per task and version (`SNAPSHOT_VERSIONS`, 2; the one at the lowest seq goes), so workers of two versions during an upgrade do not replace each other's. A 20,002-event log claims in 21 ms instead of 120 ms on sqlite. - `noeta worker --lease` and `--snapshot-every`; one worker id `host:pid:random`. - `make cluster`: worker processes on Postgres under random SIGKILL / SIGSTOP with a scripted provider (tool calls, background jobs, sub-agents, timers, Cron, mid-run messages); a CI job runs it. Example plugins `examples/run-tests`, `examples/chat-bridge`, `examples/chat-approvals` and `examples/house-style`, run by `make example`. Docs: `docs/extending.md`, `docs/server.md`, `docs/architecture.md`, `docs/adr/`. ### Fixed - **A server name in several config places starts the one `noeta mcp` shows.** The runtime took a project's `.noeta/mcp.json` first and the user's settings before the local ones, so a user server beat a local one of the same name while `noeta mcp get` showed the local one. Both now follow Claude Code: managed > `--settings` > local > the project's `settings.json` > `.noeta/mcp.json` > the user's (`noeta/toolkit/mcp_sources.py`); `--mcp-config` still wins over every file, as in Claude Code. - **Deny rules now hold against shell commands** (spec `2026-11-03-shell-deny-paths`): a `Bash` or `Monitor` command that writes a path an `Edit` deny rule covers (`mkdir`, `touch`, `rm`, `rmdir`, `mv`, `cp`, `sed -i`, `tee`, an output redirection) or reads one a `Read` deny rule covers (`cat`, `head`, `tail`, `grep`, `rg`, `awk`, `sed`, `jq`, `git diff` and Claude Code's other path-checked readers, a `cp` / `mv` source, a `<` input) is refused in every mode, `bypassPermissions` included, as Claude Code 2.1.312 does, and more widely than it: past comments, heredocs, subshells and substitutions, through globs, links, `xargs` / `sudo` and `env -C`, and a recursive `rm` / `mv` of a directory holding a denied path (`parity/deviations.md`). Before, `cd config && sed -i ... prod.toml` changed a file `Edit(./config/prod.toml)` denies, and `cat .env` read a file `Read(./.env)` denies. - **`noeta -p` no longer ends without its result.** The result's usage is read from the store after the turn closes; a read slower than half a second let the run end with no `result` line and exit 1, though the task had finished. - **A long output a tool saved itself is never saved over.** Past one reply's 200,000 character budget the runner saved a `Bash` / MCP / web result's preview to the tool's own file, losing the full output; the runner writes `.result.txt` and leaves an already saved result alone. When its save fails the result stays whole. - **With a remote exec env, auto memory lives in the env**, where the model's file tools write it; the memory plugin read the worker's disk. - **A claim whose commit landed unseen runs at once.** When the connection dropped during a claim's `COMMIT` (`CommitUnknown` on Postgres) the worker only logged it; if the claim had landed, the task sat `running` until its lease ran out. The worker now asks the store for the tasks it owns but does not run and takes one up. For store authors: `Store.tasks` takes `owner=` (the worker holding the claim). - **A channel's thread outlives its task.** `deliver` posted every later message of a thread into its task even after the task ended (failed, cancelled, or a one-shot agent's), where nobody read it. Once the task ended, the next message starts a continuation (`.2`, `.3`, ...) from the previous task's conversation; `deliver` returns the id it posted to, and `noeta.core.channel.current_task` names the thread's task now (the `chat-approvals` example answers there). - **A worker whose clock is behind the store's no longer spins on a timer.** The store wakes a waiting task when its timer is due by the store's clock (the database's on Postgres); a worker running behind found the timer not yet due and wrote the same `Waiting` again, which left the task claimable at once, over and over until its own clock caught up. A task whose log still ends with that exact `Waiting` now fires the timers due by then. - **A blocked prompt never reaches memory recall.** With a `recall_model`, the memory plugin asked the recall model about a message when it arrived, before `UserPromptSubmit` checked it, and the recalled memory stayed in the conversation after the erase. Recall now runs just before the model call that first carries the message; with `hooks` listed before `memory` (as in the built-in agents), a blocked prompt is never sent to it. With `memory` listed first, the recall made for a blocked prompt is erased before the next model call (an erase now also takes a `memory`-origin message) and its files may be recalled again. - **Foreground `Bash` commands no longer count toward the background job cap.** Eight running foreground commands of one tree refused any `run_in_background` start with a `TaskStop` hint that could not stop them. - **A foreground `Bash` command returns when its shell exits.** `sleep 20 & echo hi` waited until its timeout because the backgrounded child held the output pipe; as in Claude Code, the call now ends with the shell (the child keeps writing to the log). - **A background job's output file outlives the job's entry.** Past 64 ended jobs per root the oldest were forgotten with their logs, which their end notices told the model to read; the log now stays until the root ends, at most 256 MB of such logs per root (`Registry(let_go_logs_max_bytes=...)`). - **Several `Agent` calls in one reply count once against `max_background`.** The second of two calls with a cap of two was refused: the tool checked the reply's limits again with its sibling already running. - **A timed-out command's backgrounded grandchildren are killed even after the command exited.** `LocalExecEnv` signals the process group by the child's pid; looking the group up failed once the child was reaped, and `sh -c "sleep 77 & exit 0"` left `sleep` running. - **A `context: fork` skill's `` !`command` `` lines run only after its `Skill` call passed every check.** A `Skill(name)` deny rule came too late: the commands had already run. - **`SessionStart` (`compact`) context survives a `UserPromptSubmit` block in the same call**, and a `SubagentStop` prompt or agent hook judges a transcript without the pending hand-back call (the model refused the request, so the hook could never block). - **An `Agent` call refused for its queue makes no worktree**, and a sub-agent's hand-back that a crash cut short reports to its caller once (the post carries the call's idempotency key; the tool recovers by handing back again). - **An MCP sign-in no longer hangs the worker while a token refresh holds the token file's lock**: `SignIn` writes the file with `TokenStore.update_async`. - The per-input state skills and markdown commands keep for an erase, and fork skill prompts, keep the newest 32 entries; a failed MCP server resolution leaves no per-task entries. - **A command being started no longer hangs the event loop's shutdown.** `LocalExecEnv` starts its commands synchronously and reaps each with a thread of its own instead of using asyncio subprocesses: before Python 3.14, asyncio waits forever for a child whose setup was cancelled, and a loop shutting down (a worker exiting, a test ending) cancels everything at once. A command cancelled while starting still has its whole process group killed. - The missing-extra message's `uv tool install --reinstall` line names the README's extras (`tui`, `images`) with the missing one and says to list every extra wanted. Before, it named the missing extra alone, and running it dropped the extras installed earlier. - **A sub-agent is not left unclaimed behind a background command.** While a worker holds the tree (a `run_in_background` command, a monitor), starting an agent on a queue that worker does not serve fails with a tool error naming the queue instead of creating a child no worker would claim until the command ended. A plugin's `TaskView.spawn(queue=...)` on such a queue raises `ValueError` the same way. - **A plugin can no longer write a `core` record of an unknown kind.** `ToolContext.record` and `TaskView.record` raise `ValueError` for one; written, it left the task to a newer worker for good. - **A child is not stranded by a hold taken while its call finished.** The queue check above ran before the call's `PostToolUse` hooks and an extension's spawn was checked when staged; another task of the tree taking the hold meanwhile left the child unclaimed. The store now refuses such a child as it is written: the call fails with the same tool error, and an extension's refused spawn drops what its hook staged with a warning. - **A reminder that erases messages no longer misaligns the request.** The request after a round of reminders takes its messages from the log; an erase recorded among them left the erased message in and the reminder out. - **A worker missing a newly installed plugin no longer fails tasks during a rolling install** (see the pinned plugin set under Added), and a core log record of a kind this version does not know is left to a newer worker instead of being skipped. - **A prompt a `UserPromptSubmit` hook blocks is erased.** It stayed in the conversation, so the next turn's request carried it to the model; Claude Code erases it. The hook records a core `erased` entry and the fold drops those messages (`TaskView.erase_messages`). - **`Bash`: a command that opens fd 3 keeps its file, and the shell's directory.** The exit trap wrote the final directory to fd 3, into the command's own file; it now uses a descriptor out of the command's way (fd 254). - **`Bash`: a foreground command past the 64 MB log cap shows its real end.** Its log kept only the head, so the result showed output from the middle; a foreground log now keeps its start and a rolling tail. - **A resumed agent's worktree is no longer removed when an old job in it ends.** The note about a worktree kept for a running job now says a worker restart leaves it in place. - **An agent's `project` / `local` memory survives `isolation: "worktree"`.** It lived in the temporary worktree; it is now under the repository's main checkout. - **Typing `/skill args` again for an active skill sends it again** with the new arguments (a short note when nothing changed), as the `Skill` tool does. - **Skill directories are scanned once a turn**, not after every `Read`, `Write`, `Edit` and `NotebookEdit`. - **`Edit` and `Write` no longer stall the worker on huge files.** The diff for front ends is worked out in a thread and skipped past a million characters. - **An MCP OAuth refresh that fails on the network keeps the sign-in.** Only `invalid_grant` clears the saved tokens, and concurrent refreshes no longer spend a rotated refresh token twice. - **`Read` refuses a notebook over 100 MB before reading it.** - **`Edit` matches a multi-line `old_string` with straight quotes in a CRLF file** whose text has curly ones. - **`function_tool` accepts a positional-only `ToolContext` parameter.** - **A blocked prompt's `/command` or `/skill` expansion is erased with it.** The expanded body (arguments and `` !`command` `` output filled in) stayed in the conversation and reached the next request; the `erased` record now also drops the `inline` text recorded from the blocked input on. - **`UserPromptSubmit` checks a message sent mid-turn before the model sees it, once.** It ran only at a turn's first model call, so a `when="now"` message (or one held behind open calls) reached the model unchecked, and a block at the next turn erased it after the model had answered it. It now runs before any model call that carries new messages. - **A blocked `/skill` or `/command` leaves nothing behind.** The skill stayed active with the blocked arguments (re-attached after a compaction; a later `Skill` call with the same arguments got the "already loaded" note) and the command's `allowed-tools` stayed pre-approved. For plugin authors: a reducer whose plugin holds state is also folded with `records.ERASED_KIND` (`{"seqs": [...]}`) when messages are erased. - **A `UserPromptSubmit` hook added to a running task checks only the messages the model has not answered.** Its first run checked the whole history, and a block erased messages the model had already answered. A blocked message released with a stop's interruption marker no longer erases the marker. - **`Bash`: a foreground command whose tree hold cannot be taken (a store error) is killed at its timeout** instead of being left running unmanaged. - **`ToolSearch` scores each query term on its own:** a term found only in a tool's whole name (`ack__send`) counts even after an earlier term scored. - **`Bash`: a fast foreground command's output is kept about 3x to 5x faster.** The log keeper checked the file's size after every 64 KB read (two processes each); it now takes up to 16 reads per check while reads come back full. - **`Bash`: a foreground command moves to the background past its timeout even with the root's job slots full**, instead of being killed. - **`function_tool` checks scalar arguments** (`str`, `int`, `float`, `bool`, optionally `| None`): numbers and `true` / `false` sent as text are converted, and anything else is a tool error naming the parameter. - **Two envs with the same worktree path no longer see each other's jobs.** The jobs working under a worktree, and the removals waiting for them, are keyed by the filesystem the path names (this machine, or a container) as well as the path. - **An inbox item this version cannot read no longer fails a turn on every lease.** One a newer version posted lets the lease run out with nothing written (a newer worker takes the task); a malformed one ends the task `unreadable_log` after the usual claims. - **A spent budget refuses every model call.** It was checked after the request hooks, so `/compact`'s and an automatic compaction's summary ran past it, and an extension's call (`TaskView.complete`) was charged but never refused; that call now raises `BudgetSpent` (`noeta.core.errors`, a `Fatal`). - **`ExitWorktree` does not remove a worktree a background job runs in** (it says to stop the job with `TaskStop` first), and an agent's worktree kept for a running job is removed, if unchanged, once the job ends or is killed with its root. - **`Workflow` saves its script through the task's env**, so `scriptPath` finds it in a container or remote env. - **A read-only call that fails cancels the calls running beside it** instead of leaving them running unowned. - Smaller: `complete_authentication` on a cancelled sign-in; the MCP prompt commands cache is kept for at most 256 partitions; `ReadMcpResourceDirTool` runs again after a crash; `function_tool` takes `ToolContext | None` and positional-only parameters; an ended background job stops listening for its hold being lost (`TreeHold.on_lost` returns a call that takes the callback back). - **A sub-agent is created on its caller's queue.** Children went to `default` unless a queue was named (`Subtasks(queue=)`, a roster entry's `queue`, `spawn(queue=)`), so a task on its own queue (a GPU worker's) had children only other workers ran, and a tree held by one of them waited for a worker that never came. - **A tree is held only by a worker that can run all of it.** A worker serving one queue could hold a tree (for a background job) whose root was on another; other workers skip a held tree, so a `Cancel` or message to the root waited as long as the job ran. `Runtime.hold` now raises `HeldElsewhere` while an unended task of the tree is on a queue it does not serve. - **A worker leaves a log a newer version wrote to a newer worker.** An event, block or value it does not know (msgspec's "Invalid value" / "Invalid enum value") counted toward ending the task `failed (unreadable_log)`; the worker now lets the lease run out without counting, so a newer worker claims it. A corrupt or misshapen log still ends failed after three claims. Workers from this release on do this; 0.x workers still fail such a task (see Changed). - **A hold given back after a store hiccup is asked for once more.** One failed unhold left the hold to run out and be reaped, which told the tree (`hold_lost`) that a background job that had ended normally was lost. - **A worker's count of unreadable logs is bounded.** A task whose log failed to load once and then ended elsewhere kept its entry in the worker for good; the 1,024 most recent are kept. - **`WebFetch` no longer says a body was "saved to None".** A raw body of a text type (from a custom transport) with no spill directory, or one that could not be written, was reported, and cached, as saved to `None`; it is now an error, not cached. - **`NotebookEdit` refuses a `cell-N` that names no cell object.** On a notebook whose N-th entry in `cells` is not an object, the call failed with an `AttributeError`; it now returns a tool error. - **`ListAgents` says when its session list may be incomplete.** It lists the open sessions among the store's 200 most recently active tasks and said nothing of older ones; when that window is full it now says so, and that an older session can still be messaged by its id. - **`SendMessage(notify_when_idle)` finds any open session.** It looked for the session among the store's 200 newest tasks, so a session behind them was "unknown"; it is now looked up by id (`ToolContext.task`, new, for plugin authors). - **`function_tool` leaves `*args` and `**kwargs` out of the schema.** They became parameters named `args` and `kwargs` the model was told to fill. - **A sync `function_tool` runs off the event loop.** It ran on the worker's event loop, so a blocking function stalled every other task of the worker; it now runs in a thread. - **The MCP plugin keeps a bounded number of tasks in memory.** Its per-task entries (the resolved servers, their tools, roots and scope) were dropped only when the task waited or ended here, so a task another worker took, or one left by a crash, stayed for the process's life; past 256 tasks the least recently resolved are now forgotten. - **A skill's `allowed-tools` grant names any tool.** With `tool_enforcement` on, the grant kept only names from a fixed table that had drifted (it lacked `Glob`, `Grep`, `Workflow`, the worktree, schedule and plan tools, and every MCP tool), and a value in Claude Code's space-separated form or `*` granted nothing. The grant is now the tools the skill's rules name, read as its pre-approval reads them (`mcp__server` covers that server's tools, `Read` covers `Grep` and `Glob`, `*` everything). - **Without background jobs, `Bash` runs a long leading `sleep`.** `Shell(background=False)` refused it with a message pointing at `Monitor` and `run_in_background`, which that host does not offer; the refusal now applies only where background jobs are offered. - **An agent's worktree is not removed under its background command.** An isolated agent that ended while a background job it started still ran in its worktree had the worktree force-removed under the job (it looked unchanged); it is now kept, and the completion notice says why. - **An isolated agent's worktree goes when its caller ends first.** A task that ended or was cancelled while its `isolation: "worktree"` agents ran never heard their ends, so their worktrees stayed; they are now closed when it ends (an unchanged one removed). - **A plugin's markdown command fills its arguments as a skill does.** It replaced only `$ARGUMENTS` and otherwise appended what was typed after a blank line; `$0`, `$ARGUMENTS[0]` and `\$` now work as in Claude Code, and arguments no placeholder took follow the body on an `ARGUMENTS:` line. - **A long-lived worker keeps a bounded number of task trees' background jobs.** A tree's ended jobs and their logs stayed in the worker's table until its root ended here, so a server's idle sessions and trees taken by other workers piled up. A tree whose hold this worker lost now leaves with its logs, and past 256 trees the least recently used one with no running job does. - **A long session keeps a bounded number of background logs on disk.** An ended background job let go of (beyond the 64 most recent of a root task) kept its log files, up to 64 MiB each, until the root task ended; they are now removed when it is let go. - **`ExitWorktree` says when the branch was kept.** It reported the worktree's branch deleted even when `git branch -D` refused; it now says the worktree was removed and the branch kept, with git's reason. - **`ToolSearch`'s `+word` filters on tool names only.** It also kept tools whose description held the word, which its description (and Claude Code's) rule out. - **`ToolSearch` takes an infinite `max_results`.** `1e400` (an infinity once parsed) made the call fail with an `OverflowError`; it now means the default of 5, like any other unusable value. - **Front ends attached to a server work without a local model.** `noeta --server URL` exited with "no model configured" unless this machine named one, and its new tasks (and `noeta acp --server`'s) carried this machine's directory, which the server refuses (403 outside its directories), and the TUI's model, which overrode the server's. They now send a model only with `--model` and a directory only with `--cwd PATH` (a directory on the server; a relative one is taken from the server's); otherwise the server records its own directory in the task and the agent's model applies. `noeta -p --server` ran locally and silently ignored `--server`; it now refuses it. - **A hold whose commit was cut off is no longer left behind.** When the store's connection dropped while a hold committed (`CommitUnknown`), a hold that had landed was neither registered nor renewed: the tree stayed unclaimable until the hold ran out and its reaping told the tree `hold_lost` for nothing. `Runtime.hold` now asks again (up to three times); taking a hold extends the one the worker has, so a hold that landed comes back with its epoch and is renewed and given back as usual. - **An agent's `denied_tools` / `allowed_tools` also cover tools a plugin adds from the others' (`Extension.tool_changes`).** An agent that denied `ToolSearch` was still offered it and ran it. An added tool the lists refuse is now neither sent nor run, and the rest of that plugin's changes go with it; the deferred tools it withheld are sent as plain tools and not announced (an agent that denies `ToolSearch` gets its deferred tools up front, as Claude Code does when its search tool is unavailable). `TaskView.deferred_tool_names` also leaves out denied tools. - **Crash recovery no longer removes another agent's worktree.** An isolated agent's worktree was named after the last characters of the call id, which is unique only within its task, so a crashed call's recovery could force-remove the worktree of another task or session in the same repository. The name now comes from a hash of the task id and the call id, and recovery keeps a worktree holding uncommitted files or commits. - **A worktree-isolated agent can commit with the sandbox on.** Its worktree's git data lives in the main repository, which the sandbox kept read-only (`index.lock: Read-only file system`). The worktree's own git directory and the shared `objects`, `refs` and `logs` are now writable; hooks, config and the files that point git elsewhere stay read-only (`parity/deviations.md`). - **Removing a worktree no longer drops commits left on its branch.** The unsaved-work check (`ExitWorktree remove`, an isolated agent's end) counted only commits on the worktree's current `HEAD`, so after a switch to another branch the commits on the `worktree-*` branch were lost with `branch -D`. It now counts both. - **A long-lived conversation no longer piles up ended background jobs in memory.** The worker kept every ended background job of a root until the root ended; it now keeps the last 64 once their end is posted (older logs are removed with the root's), and the list of ended tasks forgets its oldest entry first instead of an arbitrary one. - **Two writers of one MCP sign-in no longer lose tokens.** The OAuth token file was written through one fixed `.tmp` name and updated read-modify-write without a lock, so two workers refreshing at once could drop a field or fail on the rename. Each write uses a temporary file of its own and updates hold a file lock (`.lock`; no lock on Windows). - **Two per-worker caches are bounded.** The shell plugin's sandboxed tool lists (one per sandbox settings and working directory) and the agent rosters resolved per set of named agents grew for the life of a worker; each now keeps the 64 used last. - **A sub-agent that ends without handing back reports with its final message.** In auto mode a sub-agent that ended without `SubagentHandback` came back as "no report" and its last reply was dropped: one whose agent definition lacks the subtasks plugin, whose tool list leaves the tool out, that runs on another worker's queue, or that simply answered in plain text. Its final message is now its report, as in Claude Code. - **An unknown model is announced once.** The front end's notice now also says its cost counts as $0.00, and the catalog no longer logs a second "not in the catalog" warning about its price. - **A task whose log cannot be read no longer loops forever.** A log no claim could read or fold left the task `running` to be claimed again on every lease; after three such claims in a row on a worker it now ends `failed` (`unreadable_log: ...` with the last error), its parent gets `ChildEnded` and its children are cancelled. Only a failure to load counts: one raised later (failing the turn failed too) is logged and the task claimed again. - **An interrupted reply keeps the thinking it finished.** Only the streamed text was kept; completed thinking blocks (signature included) now go back ahead of it (`TurnStopped.thinking`; older records fold as before). - **A background job's end is no longer lost around a sub-agent's end.** A sub-agent could end without reading `hold_lost`, and a job finishing just as its sub-agent ended posted its end to an inbox no one reads; both now reach a reader (see Store contract). - **The terminal UI no longer stays "working" after a fast turn.** It marked itself busy only once the message was sent, so a turn that ended (and was shown ending) before the send returned left it busy until the next one; it is now busy from the moment it sends. - **A command missing its optional dependency says which extra to install.** `noeta serve`, `noeta acp`, the terminal UI, a Postgres `--store` and the other extras' imports exit with `pip install 'noeta[]'` instead of a `ModuleNotFoundError` traceback. - **A fork of a fork keeps the whole conversation.** A task continuing another (`history_from`) folded only that task's own log, so a second rewind, or a sub-agent resumed twice through `SendMessage`, lost everything before the first continuation. - **A continued task's conversation no longer moves.** A task created with `history_from` and no `history_through` (`--fork-session`, a resumed sub-agent) folded its source's whole log on every load, so it changed whenever the source went on. Creating it now records the source's last seq (`noeta.core.store.pin_history`). - **A continued task keeps the tools its source loaded** with `ToolSearch`; they were no longer sent while the history still referenced them, and calls to them were refused. Calls its source left open are answered right after their `tool_use`, ahead of messages held behind them (the API refused the first request of a fork taken with an interjection). - **A continued task leaves its source's timers and jobs behind.** Plugin state was copied whole: cron jobs and `/loop` wake-ups were listed in the new task and never fired, a running workflow could be driven twice. `Extension.continue_state` decides per plugin; schedule starts empty, workflow keeps finished runs, shell its cwd and finished jobs. - **`restore_files` puts back only what the task snapshotted.** The signal (also open as `POST /tasks/{id}/signals`) wrote any content to any path. It now writes a file inside the workspace only when the task's log snapshotted it and the content hashes to one of its snapshots — snapshots of this task, of the conversations it continues and of the sub-agents under them (`TaskView.read_log`, `TaskView.child_tasks`), so a rewind still puts back what a sub-agent edited. A rewind restores through the task the front end goes on with (the fork, when the conversation is rewound), which shows the notice. - A timer set again for the same name and time fired only once, and the task then spun; `TaskView.spawn` without a `key` reused the first child's id; children `view.spawn` staged in `after_tool` were never created. - **An "always" shell approval no longer widens to the program and its first argument.** Approving `rm -rf build` used to let `rm -rf /` run unasked (and wrote that rule to a trusted project's `.noeta/shell-allowlist.json`). The remembered rule is now Claude Code's `Bash(...)` form, seeded as Claude Code seeds it (`git push *`, `ls *`), and the exact command for wrappers, destructive file programs and interpreters. The approval card (TUI) and the ACP option name the rule (`Always allow Bash(git push *)`), and no "always" choice is offered when nothing would be remembered. Approval details carry `always_label`. - **A command that only reads with `git` cannot run a repository's helper programs.** It runs under a `git` shell function that turns off the fsmonitor hook, hooks, pager, signature programs and transports, and adds `--no-ext-diff --no-textconv`, wherever it reads (`-C`, `cd`). Unless a person approved the command, the function also refuses a repository whose git directory lies inside the workspace without being a `.git` directory or the one a `.git` file names (a bare one committed into a clone); a bare repository's worktree, `--separate-git-dir` and git directories outside the workspace read normally. Deny and ask rules match a command's words as they run (`\rm`, `'rm'`, `env rm`, `git -C . push`); reads are judged where symlinks lead, and a recursion that follows every link it meets (`grep -R`, `rg -L`, `find -L`, `du -L`, `tree -l`, `ls -LR`) asks, as does a glob that takes in a link out of the workspace (`cat *` beside `k -> ~/.ssh/id_rsa`: the glob is expanded as bash expands it and each match resolved); a write under a link an earlier part of the same command copies asks. Auto mode keeps exact `Bash(...)` allow rules. **Known limit:** a repository directory unpacked from an archive (not cloned) brings its own `.git/config`, whose `filter..clean` program `git status` and `git diff` still run; git has no setting that turns every filter off. A command a person approved runs hardened but without the planted-repository refusal, as Claude Code runs an approved command as written. - **An untrusted project no longer configures the agent.** `noeta` read `/.noeta/agents` unconditionally, so a cloned repository could replace `main` with one in `bypassPermissions` running a command hook. A project's `.noeta/agents`, `.noeta/prompts`, `.noeta/skills` and `.agents/skills` now load only after `noeta trust`, like its `.noeta/plugins`; each one skipped is a notice. `Runtime.workspace_trusted` gates the workspace's prompt overrides (`None`, the default, trusts every workspace). - **A sub-agent ends after its turn**, like Claude Code's, whatever its agent file says. An agent file without `one_shot: true` (every Claude Code agent file) used to leave its sub-agent waiting for input and its foreground parent waiting forever. The main agent stays conversational. - **An image that the API would refuse no longer enters the history.** `Read` compared raw bytes with 5 MB, so a 4–5 MB image (over 5 MB once base64 encoded) failed every later request of the task. Images from `Read` and MCP results now go through `noeta.toolkit.images.fit_image` with Claude Code's limits: shrunk into 2000 x 2000 px and 3.75 MB raw with Pillow (the new `images` extra), else passed only when already within 5 MB of base64 and 8000 px a side, and refused with a reason otherwise. A scaled image's result says how coordinates map back. `Read`'s image file ceiling is now 128 MB (`IMAGE_MAX_BYTES`); `noeta.plugins.mcp.results` no longer exports `IMAGE_MAX_BYTES`. - **Long tool output is saved, as the tools promise.** No built-in host set `Runtime.spill_root`, so Bash, WebFetch and MCP output past the inline limit was cut instead of saved to a file. `LocalApp` now defaults it to `/noeta-/tool-results` (`noeta.host.local.default_spill_root`) and the `noeta` command (TUI, `-p`, ACP, `serve`, `worker`) to `~/.noeta/tool-results`; one directory per task below it. - **MCP results and timeouts follow Claude Code.** A result is held to 25,000 tokens (`NOETA_MAX_MCP_OUTPUT_TOKENS`; four characters a token, 1,600 an image) instead of 1 MiB: images past it are left out with a note, text past it is cut to a marked head and saved. A tool call is bounded by the server's `timeout` (milliseconds, read from settings files now; under 1000 ignored), else `NOETA_MCP_TOOL_TIMEOUT`, else 1e8 ms, instead of 30 s; every other request by `NOETA_MCP_TIMEOUT` (default 30 s). `noeta.plugins.mcp.results` no longer exports `INLINE_MAX_BYTES`. - **Skill arguments are filled in, not dropped.** A skill body's lines holding `$ARGUMENTS` were removed. The `skill` tool now takes Claude Code's optional `args` string, and `/name args` passes what follows the name; `$ARGUMENTS`, `$ARGUMENTS[N]` and `$N` (from 0) are filled as Claude Code fills them (`\$` keeps a `$`), arguments no placeholder took follow the body on an `ARGUMENTS:` line, and a call without `args` leaves the body as written. The arguments are kept with the activation, so the body re-attached after a compaction is the same (`noeta.plugins.skills.registry.substitute_arguments`). - **Model catalog: the Claude 5.5 family and Fable 5.1.** `claude-opus-5-5`, `claude-sonnet-5-5`, `claude-haiku-5-5` and `claude-fable-5-1` are catalogued with Claude Code 2.1.312's windows, output caps and prices, and the aliases follow Claude Code (`opus` / `sonnet` / `haiku` -> the 5.5 family, new `fable` -> Fable 5.1). An uncatalogued model is now a notice the `noeta` command shows (`noeta.core.models.unknown_model_notice`), not only a log line. - `noeta -p` announced an uncatalogued model three times (the loader's log, the front end and the catalog's own warning); it says it once. - `make example` ran a build uv had cached; it now installs the example plugin editable. - A second `registry.provider()` call for a family failed once that family's adapter module had been imported (the entry point named a package attribute the submodule rebound). - **No request switches a served task into `bypassPermissions` through an answer**: HTTP answers (and the stream-json front) go through the same guard as create and signal; send and cancel do too. - **Crashes at the wrong moment no longer lose work.** A worker dying right after a timer fired no longer drops a recurring Cron job, a fired one-shot job's deletion or a `/loop` fallback; a root that dies after taking `hold_lost` still tells its ended sub-agents' lost jobs; a parked MCP call whose worker died ends at once instead of when someone answers; a job of an ended sub-agent that is lost is told to the root; a finished job is no longer recorded lost when another worker takes its end; a background job counts as running until its end is posted. - A timer due while a turn is parked fires once the turn ends (as Claude Code fires only when idle); documented and tested. - `--continue` found no conversation behind more than 200 newer tasks. - The TUI's status-bar cost is the whole task tree's, as `/cost` reports it, and resets when another task opens; the TUI no longer shows the hooks plugin's own `retry` records as model retries; every front end builds approval cards the same way (`host.pending`). - After `/compact`, what is given again goes ahead of the command, and a local command's tags reach the model as one block each with the next prompt joined to them, as Claude Code sends them. - Workflow end notices keep their dedupe key. - MCP prompts and resources (`list_prompts`, `expand_prompt`, `list_resources`, `read_resource`) use the pool they are given even while it holds no connection; before, an empty pool was passed over for the process-wide one. - A worktree-isolated agent resumed with `SendMessage` after its unchanged worktree was removed runs in its caller's directory; before, it was started in the deleted directory and its end reported work "kept" at a path that no longer existed. - A foreground `Bash` command moved to the background is no longer killed after 30–60 s when the worker's lease is longer than 90 s: its liveness file is re-created as often as its guard checks it, not at the slower pace of the lease. - The output file a lost job's notice names still exists: a worker sweeps a dead worker's log directory only a day after its last write, not as soon as it prepares its own. - `ExitWorktree` with `remove` removes the worktree `EnterWorktree` made even after the shell switched between two other worktrees; the second switch forgot it. - An `Agent` call with `isolation: "worktree"` that a crash cut short, after its worktree was made but before its agent launched, no longer leaves that worktree behind: the call's recovery removes it. - A command hook whose run is cancelled (its turn stopped) is killed with its process group, as a timed-out one was; its output is capped while it is read, not after. - Compaction's request estimate (and the files restored after it) and the MCP result budget count a CJK character as one token, as the other budgets do; they counted four characters a token, so Chinese, Japanese or Korean text was undercounted three- to four-fold. - MCP sign-ins that ended are let go three minutes after they started, instead of being kept for the life of the worker. - `noeta -p` in an untrusted directory no longer warns that it skips `.agents/skills` and `.noeta/skills` when neither exists; the warning names only the skill directories that do. - MCP: a cancelled task that started a server's connection no longer cancels the other tasks waiting on it; a call stopped by an interrupt or a cancel sends the server `notifications/cancelled` (before, only one that timed out did); a Streamable HTTP server that ended the session (a 404) gets a fresh connection on the next use instead of failing every call; closing a stdio server ends its whole process group, and a server that stops reading its input breaks the connection after the timeout instead of hanging the writer. - `ExitWorktree` counts untracked files when git is set to hide them (`status.showUntrackedFiles=no`); such a worktree was removed with its new files. - Auto mode: past the classifier's denial limit, a task nobody can answer (`interactive: false`, no prompt tool) is refused the call instead of parking on a question for good. ### Security - **An `@path` mention attaches only what the model's own `Read` could read without asking** (the working directories, an allow rule); a file it would ask about is named instead, so the model's `Read` goes through approval. A channel's message is never expanded: `deliver` now frames every message not framed yet (`source=`, `sender=`; `from_channel` tells), as Claude Code's channel messages arrive. `~` is expanded only with `FileTools(home=)` (set by the `noeta` command's front ends, never `serve` or `worker`), and no `PreToolUse` hook command runs for the attachment. Before, any chat member could have a worker read `~/.ssh` or `~/.noeta` into the conversation. - **The memory directory's write and read exceptions are judged where the file lands**: a link planted in a project agent's memory directory no longer writes `.noeta/agents/` without asking. - **An image over 50 million pixels is refused before it is decoded** (a small PNG could take gigabytes); Bash and MCP images are decoded off the event loop. - **A skills directory git ignores (a dependency's) is never discovered**, and a skill found below the working directory never replaces one of the same name. - The permissions plugin decides whether a workspace is trusted from each task's own working directory (`noeta trust`), as the MCP and skills plugins do. Before, the `noeta` command took the trust of the directory it started in for every task, so a server started in a trusted project treated tasks in `--allow-root` directories as trusted: the auto-mode classifier read their `NOETA.md` as the person's intent, and "always" answers were written into their `.noeta/settings.local.json`. `Permissions(trusted_workspace=)` also takes a `(cwd) -> bool`. - `LocalExecEnv.read_bytes` / `read_range` open without blocking and refuse anything but a regular file (a directory still raises `IsADirectoryError`). Before, reading a FIFO a command had made held a worker thread until something wrote to it. - The end of a `noeta serve` event stream (`/tasks/{id}/watch`) and stream-json's "lost the task's events" result no longer carry an error's raw text: as for other requests, a client gets the text only of an error noeta raised itself, else a generic message (the server logs the error). - The Linux sandbox's filter refused `socket(AF_UNIX)` but not `socketpair(AF_UNIX, ...)`, whose datagram sockets send to any Unix socket by address (the host's journald). It is refused under the same rule, unless Unix sockets are allowed. - `noeta serve` takes a structured `input` (`POST /tasks`, `/tasks/{id}/messages`) only as a user turn of text and images, and always as the person's (`origin` `human`). Before, a client could send an assistant or system turn, tool calls and results, or a user turn marked as another agent's or a system notice, forging the conversation and skipping the `UserPromptSubmit` hooks (they look only at the person's turns). Anything else is a 400. - **A read-only `git` no longer runs the filters of a repository planted outside the workspace.** A command that only reads with git already ran with the repository's hooks, fsmonitor, pager, signature program and external diff turned off, and refused a git directory written as plain files inside the workspace (`git -C evil status`). In another directory the model may write without asking (an additional working directory), such a repository with `core.worktree` and a `filter..clean` still ran the filter on `git status` or `git diff`, auto-allowed as read-only. A git directory that is not `.git` (nor named by a `.git` file) is now refused anywhere unless it is bare. - The git noeta runs itself for worktrees (`EnterWorktree`, `isolation: "worktree"`) runs no hook and no `core.fsmonitor` command (`-c core.hooksPath=/dev/null -c core.fsmonitor=`), so what a command planted in the repository does not run as the worker; the sandbox's lookup of the hooks directory runs no fsmonitor either. - **A sandboxed command can no longer point git at other config or hooks.** It could write `.git/commondir` (git then takes config and hooks from the directory it names) or a submodule's git directory under `.git/modules/`, and the next unsandboxed `git commit` or `git worktree add` ran the planted hooks. `.git/modules/` and an existing `.git/commondir` are now read-only (an empty placeholder covers a missing `.git/modules/`), as are each linked worktree's `commondir` and `gitdir` seen from the main checkout. A missing `.git/commondir` cannot be covered (git dies on an empty one, on the host too): on Linux one the command creates is removed when the command ends; Seatbelt refuses creating it. - **A sandboxed command can no longer reach into the network relay it runs under.** The relay inside the Linux sandbox (same user, same PID namespace, no system-call filter, and holding the proxies' Unix sockets) was dumpable, so where `kernel.yama.ptrace_scope` is 0 a command could trace it, and under any setting read its `/proc` files. The relay is now not dumpable, and the command's filter refuses `ptrace`, `process_vm_readv`, `process_vm_writev` and `pidfd_getfd` whatever the settings allow. - The sandbox's filter refused `socket(AF_UNIX)` but let a command set up io_uring, which creates sockets without `socket(2)` on Linux 5.19 and later. `io_uring_setup`, `io_uring_enter` and `io_uring_register` are now refused unless Unix sockets are allowed. - **An agent hook's tool calls go through the task's permission checks.** It ran every read-only tool (`Read`, `WebFetch`, `WebSearch`, read-only MCP tools) with no check; now a deny refuses the call and so does anything that would ask, as Claude Code runs an agent hook in `dontAsk` mode. - On Linux machines other than x86_64 and aarch64 the sandbox's system-call filter (no Unix sockets, no new user namespaces) was silently left out. The sandbox now reports itself unavailable there unless the settings allow both, so `failIfUnavailable` refuses the commands. - `noeta plugin add` takes only a full commit id (40 hex digits, or 64 for a SHA-256 repository) and only `https://`, `ssh://`, `user@host:path` or a local `file://` source, for a plugin and for a marketplace and its entries. Before, a 7-character prefix (which a commit pushed later could share) and `http://` or `git://` (which anyone on the way could answer with other code) were accepted. - An instruction file's `@import` is read only when it resolves inside the importing file's directory or the working directory. Before, the check used the path as written, so a repository's link (`docs/x.md` -> `~/.ssh/id_rsa`) put the target's text into the model's context. - **noeta's home and the sqlite store are owner-only when noeta creates them.** `~/.noeta` (`NOETA_HOME`) was made 0775 and the store's database with its `-wal` and `-shm` files 0644 under the usual umask, though the store holds every conversation. noeta now makes its home 0700 and a new database 0600 (SQLite gives the side files the same mode). Existing ones keep their mode; a front end, worker or server whose home or store is open to other users says so at start, with the `chmod` to run. - `noeta serve` takes only `model`, `effort`, `max_turns`, `cwd`, `permission_mode` and `output_schema` in a new task's `config`, and only `model`, `effort` and `max_turns` in a `config` signal; any other key is refused with 403 naming it. Before, a request could set the permissions plugin's inherited allow rules (`permission_rules`) or `subtask_depth` and run tools without being asked. - `POST /tasks/{id}/signals` takes only the signals front ends send (`permission_mode`, `config`, `resumed`, `restore_files`). Before, a request could post a plugin's own signal, such as a background job's end (a forged ``) or a sub-agent's idle subscription naming another task, which then received the notice. - `GET /commands` and `POST /commands/run` check `cwd` as a new task's (outside the server's directories: 403) and, given a `task_id`, use the directory that task was created in. Before, any `cwd` was scanned for skills and could start a trusted directory's MCP servers. - Resuming or forking over `/stream-json` looks the task up through the client first. Before, resuming an unknown task id had this server serve a tree for it. - `~/.noeta/server-token` keeps only the server token's SHA-256; the token is printed once, when made (`noeta serve` no longer prints it on every start). A file an older noeta left holding the token itself is rewritten to its digest on the next start, which prints the token one last time. Clients (`noeta --server`, `noeta acp --server`) read the token from `NOETA_SERVER_TOKEN`. - The bearer token is taken from `?token=` only on the `/stream-json` WebSocket; every other route needs the `Authorization` header, so tokens stay out of ordinary URLs and their logs. - `noeta plugin add` fetches only from git URLs (`https://`, `ssh://`, `git://`, `file://`, `user@host:path`) and passes `--` before them, so a marketplace entry's `url` cannot be a git option such as `--upload-pack=…`. - The first user turn's git snapshot runs no signature program (`-c log.showSignature=false`) and does not look inside submodules (`--ignore-submodules=dirty`), so a cloned repository's `gpg.program` or a submodule's filter driver no longer runs when a task starts; a submodule with changed files but no new commit is no longer listed. - `noeta serve` logs a `?token=` value as `REDACTED` in uvicorn's access and error logs (`noeta.host.http.redact_token_logs`, for host code serving `create_app`). Before, every WebSocket connection logged its path with the token at info level (`"WebSocket /stream-json?token=…" [accepted]`). - `noeta serve --allow-host NAME` (repeatable) names the `Host` headers the server answers on any bind, added to the loopback names on a loopback bind. Without it a non-loopback bind checks no `Host`, which SECURITY.md now says (it claimed unexpected `Host` headers were always refused). - A command missing an optional extra names the install from this repository (`pip install 'noeta[server] @ git+https://github.com/initxy/noeta'`, and the same for `uv tool install --reinstall`). Before, it named `noeta[server]` on PyPI, where noeta 1.0 is not published and the name may install someone else's package. - A started workflow's script is kept in its task's own spill directory (`tool-results//workflows/`). Before, every task's went to one shared `tool-results/workflows/`. - An MCP prompt command (`/mcp____`) runs in the connection partition of the request that runs it: listed prompts are kept per partition. Before, they were kept by command name only, so after a request in another partition listed the same command, `/commands/run` used that partition's connection and OAuth sign-in. - `POST /tasks/{id}/rewind` creates the new task through the request's guard: only the config a request may set is carried over, and `bypassPermissions` and a `cwd` outside the server's directories are refused (403). A rewind keeps the task's queue (`noeta.host.rewind.rewind(config_keys=)`). Before, it copied the original's whole config through the unguarded client, so a request could rewind a task host code made with plugin keys (`permission_rules`) or a mode a request may not ask for, on the `default` queue. - `noeta serve` keeps a query-string token out of uvicorn's logs whatever way its key is spelled: `?%74oken=` (which the server decodes to `token` and accepts) is redacted too. Before, only a literal `token=` was. - **A continued task no longer keeps bypassPermissions where bypass is disabled.** A fork, rewind or resumed fork of a task that ran in bypassPermissions (on the CLI, say) kept that mode on a server started without `--allow-bypass-permissions`, and ran tools without asking. The stored mode now goes through the same check as a task's config, and a continued task's state drops bypassPermissions there. - **A shell command that writes a protected path asks in bypassPermissions.** A `Bash` command writing `.git/hooks`, `.noeta/settings*.json` or another protected path (a file command's target or an output redirection, as written or through a link) ran without asking in bypassPermissions; it now asks there, as the file tools already did. In auto mode the classifier still judges it, as Claude Code's safety check allows. - A compaction's transcript file is named from the task id with anything but letters, digits, `_`, `.` and `-` replaced, as spill files are. A sub-agent's id carries the provider's tool-use id, so a gateway answering with `../` in it could have the transcript written outside the project directory. A spill file of a task or call named `..` now stays in its directory too. - The HTTP server and `/stream-json` control requests no longer send a client the text of an error that a library or the system raised (it may name the server's paths); the client gets a generic message and the server logs the error. Errors noeta raises for the caller keep their text. - **The command sandbox protects repositories below the working directory**: git hooks and config, editor tasks and the other protected names are read-only down to three levels (as Claude Code's `mandatoryDenySearchDepth`), so a workspace holding several repositories no longer lets a command plant a hook that runs on the person's next commit. At most 2,000 directories are listed; dependency and cache trees are skipped. - A legacy SSE MCP server whose `endpoint` event names another origin is refused: every message would have carried the configured headers and the OAuth token there.