Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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
  `<tool_use_error>` 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/<YYYY-MM-DD>/<first 8 characters of its id>/`. `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: <type>: <message>`) 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 (`<plugin>.<rest>`) 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:<Tool>`, `request:<Tool>`,
  `record:<plugin>/<kind>`, `codeblock:<language>`, `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, `<git-url>@<commit>`, 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 <name>` 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.<name>]` 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/<name>/`), 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[<extra>]'` / `pip install 'noeta[<extra>]'` 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 `! <command>` line.** The main prompt's
  `# Session-specific guidance` now opens with Claude Code's rule to suggest `! <command>` 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.<name>]` 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.<name>] 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 `<tool_use_error>InputValidationError: …</tool_use_error>`. 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 `<tool_use_error>` 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: <name>`. Failures while a tool works keep `<Tool>: <message>`.
    - 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 `<fork-boilerplate>` 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 `<skills dir>/<name>/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 `<<hard_deny_extra>>`, `<<soft_deny_extra>>`,
      `<<allow_extra>>` and single `<<environment>>` slot are no longer filled: wrap each section's
      built-in text in `<<hard_deny>>…<</hard_deny>>`, `<<soft_deny>>…<</soft_deny>>`,
      `<<allow>>…<</allow>>`, `<<environment>>…<</environment>>` (`<<deny_rules>>` 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-<type>`
    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
    `<dir>/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
    `<worktree>` element, says who stopped the agent, uses the step-limit wording and omits an
    empty `<result>`, 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-<type>` 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__<server>__<tool>` 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 `<plugin>:<skill directory>`, its agents and commands
    `<plugin>:[<subdir>:]<name>`; a nested skill that shares a name is offered as `<dir>:<name>`
    (a skill file cannot name itself with a `:`, and a scoped name never replaces another skill); a
    typed `/<name>` 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 `<plugin>:...`; the model, the
      `Skill` tool, `subagent_type` and typed `/<name>` 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:
  <path>]`**, so the model can copy it into the project (Claude Code's pasted-image paths):
  `<noeta home>/image-cache/<task>/` on a local exec env (a sandboxed command may read it),
  `<temp>/noeta-images-<uid>/<task>/` 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 `/<skill>` 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
  `/<plugin>:<command>` 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, `<analysis>` then `<summary>`) 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
  (`<home>/projects/<workspace>/<task>.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/<workspace>/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 `<sandbox_violations>`.
  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__<server>__authenticate` / `mcp__<server>__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/<agent>`) 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 `<total_tokens>` 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.<name>]` (arguments,
  `enabled = false`) and `[agents.<name>] 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
  `<task-notification>` 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/<pid>` 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-<index>` 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 `/<directory>:<command>`). 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_<KEY>`. 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 `<schema>_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 <n>`; `@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 `<git-url>@<commit>` (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 `/<plugin>:<command>`), with `${CLAUDE_PLUGIN_ROOT}` and
  `${CLAUDE_PLUGIN_DATA}` filled in.
- **Channels** (`noeta.core.channel.Channel`, entry-point group `noeta.channels`,
  `config.toml` `[channels.<name>]`): 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 `<spill root>/<root>/tasks/<agent id>.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`
  (`<task id>:<call id>`, 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 `<call>.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 (`<id>.2`,
  `<id>.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 (`<file>.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[<extra>]'` 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.<driver>.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 `<cwd>/.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 `<tmp>/noeta-<uid>/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.<name>.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 `<task-notification>`) 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/<task id>/workflows/`). Before, every task's went to one shared
  `tool-results/workflows/`.
- An MCP prompt command (`/mcp__<server>__<prompt>`) 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.