Skip to content

Fix eleven session-analysis defects in run, path refusals, Bedrock, colour codes and docs - #4247

Merged
bbondy merged 15 commits into
brave:mainfrom
netzenbot:fix-session-analysis-nov15
Nov 15, 2026
Merged

bbondy merged 15 commits into
brave:mainfrom
netzenbot:fix-session-analysis-nov15

Conversation

@netzenbot

Copy link
Copy Markdown
Collaborator

Closes #4169
Closes #4231
Closes #4233
Closes #4234
Closes #4235
Closes #4236
Closes #4237
Closes #4240
Part of #4230
Part of #4238
Part of #4239

User impact: a background program that opens the terminal now fails instead of hanging, job_output waits stay under the prompt cache lifetime, step failures name the signal, file refusals in an unattended run stop pointing at /add-dir, colour codes are taken out of what the planner reads, and Bedrock tells a person when it stopped sending cache breakpoints.

Decisions to confirm

  1. A tui opener test that runs the installed xdg-open hung for tens of minutes and blocked the test run until killed #4230: the opener-test deadline is 20 seconds, and the Windows-only waits keep their old waits. Instead: a shorter deadline, or a Windows helper. Why: a loaded host needs room, and the shared helper is Unix-only. Commit: A test that runs the installed xdg-open or gio waits for it at most 20 seconds and kills it with what it started. To reverse: change the deadline constants in crates/tui/src/opener.rs.
  2. A background run on macOS leaves a program that touches the terminal stopped for good and job_output says it is running #4169: every unix stage starts in its own session with setsid, including an unconfined stage on Linux. A stage found stopped by a signal ends as a new Outcome::StoppedBySignal, and only the stage leader is checked. The unsafe pre_exec is in crates/sandbox. Instead: macOS only; reuse Stopped; read the whole group; put pre_exec in crates/agent. Why: the same hang applies on Linux, and a stopped non-leader is not a stopped stage. Commit: Start every unix stage in a session of its own and end a stage found stopped by a signal. To reverse: start non-macOS stages as a group only and drop the variant. The placement of the unsafe is still open for the maintainer.
  3. job_output waits of up to 600 s outlast the 5-minute prompt cache, so each long wait rewrites the whole prefix #4233: the longest job_output wait is 270 s with promptCacheTtl unset or 5m, and 600 s with 1h. A longer value is refused with a message naming the bound and the cache reason. A foreground run deadline has no bound. Instead: shorten silently, or bound the deadline. Why: RUN-17 forbids silent shortening, and the deadline_seconds description now states the cache cost. Commit: Hold one job_output wait under the prompt cache lifetime in force. To reverse: change WAIT_CACHE_MARGIN or Deadlines::waiting_under in crates/agent/src/exec.rs.
  4. A step ended by a signal is reported as 'was killed' with no signal named, so the cause could not be judged #4235: a signal-ended step reads step N was ended by signal 9 (SIGKILL), and an unreadable status reads ended without an exit code. Instead: keep "was killed". Why: "killed" named no cause and was also said when no signal was recorded. Commit: Name the signal that ended a step in the sentence about how a run ended. To reverse: change one arm of failed_steps in crates/agent/src/tools.rs.
  5. read_file and list_files refusals outside the workspace point at /add-dir in an unattended run where nobody can answer #4231: the run wording is kept and names no --add-dir. Instead: the wording in the issue's Fix text. Why: the run refusal and its test already name no route. Commit: File tools name no route to a person where nobody can be asked. To reverse: change NOBODY_TO_ASK in crates/agent/src/tools.rs and TRUST-10.
  6. vet_content refuses a listed file beside the workspace under bypass, where read_file would have shown it #4236: R3 is documented in the read_output spec (it opens no file slot, OUTPUT-2) and not changed in code. R5 is recorded as not taken, since opening the reference was chosen. R20 rests on manifest validation already refusing outside directory paths for list_files and search, and a new core test records it. Instead: a reach for read_output, a new listing sentence, a reach around the manifest calls. Commit: Open a reference to a listed file under bypass with the reach read_file has. To reverse: each needs a code change plus its spec sentence.
  7. A !! shell line typed while a turn runs is sent to the model as a prompt, twice, with no note that shell mode is off #4240: Enter mid-turn on a ! line keeps the line in the box with a note, and queues nothing. Instead: queue it as a shell line. Why: a pasted ! cannot be told from a typed one (SHELL-4). Commit: Enter mid-turn on a line that begins with ! holds the line in the box. To reverse: replace the new arm in crates/tui/src/app/keys.rs. The other choices are in the A !! shell line typed while a turn runs is sent to the model as a prompt, twice, with no note that shell mode is off #4240 reproduction block.
  8. A run result sent to the planner keeps terminal colour codes that the person's own view strips #4238: removal happens after the output cap cut, in one pass, and the planner sentence is English text in bravebot-core. Instead: removal before the cut, a loop to a fixed point, the sentence in the agent. Why: offsets and trailers keep naming the raw slot, and LOCALE-1 keeps planner text out of the catalogue. Commit: Take colour codes out of the output the planner reads and say how many. To reverse: move the call before bounded, or move planner_note.
  9. The /mcp reference page omits remove and the * pattern, so an answer from it said the command may not exist #4234: the * pattern is built and documented, contrary to the brief. Instead: leave the pattern out. Why: SERVERS-19 already describes it, so omitting it would leave the page behind the code. Commit: Name remove, the permanent form and the * pattern in the /mcp reference page. To reverse: delete the pattern paragraph in commands.md and its test assertions.
  10. A single 403 makes Bedrock requests drop cache breakpoints for the rest of the session, with no notice #4237: R2 is not taken and R3 is chosen. Bedrock records a refusal only after a second refusal, and the record lapses after 30 minutes. drive and the gateway and Anthropic clients are unchanged (BACKEND-57). Instead: a second send before recording, or a change in drive. Why: the issue allows either, and a count needs no extra request. Commit: Bedrock records refused cache breakpoints only after a second refusal, for thirty minutes, and says so. To reverse: change remember_at and REFUSAL_LASTS in crates/bedrock/src/lib.rs.
  11. A delegate that ends as a transport failure is reported with no cause, so the same task is started again #4239: the person-only settings BRAVEBOT_BEDROCK_MAX_RETRIES (at most 7) and BRAVEBOT_BEDROCK_IDLE_TIMEOUT_MS (at most 600000) give the tier-variable Bedrock account the retry count and idle gap a provider entry has. R21 is left blocked. Instead: no settings, or building the hand-back. Why: the bounds match a provider entry, and the hand-back needs a design decision. Commit: The planner is told when a delegate ended transport or incomplete, and to split the task. To reverse: remove BEDROCK_PATIENCE from person_only() in crates/config/src/env_var.rs.

The problem

#4230 An opener test that runs the installed xdg-open can hang the test run. Nothing bounded the wait. Left: the cause of the hang is not found, because macOS has no xdg-open and the cause needs a Linux host.

#4169 A background run on macOS that opens /dev/tty stops for good, and job_output says it is running. The state stays T and every look reports "still running". The hang through the terminal client was not reproduced. A pty harness reproduced the stop only (SIGTTOU as state T, and Errno 6 on /dev/tty once the stage had its own session).

#4233 A job_output wait of up to 600 s outlasts the 5-minute prompt cache. The next request writes the whole prefix again, and the tool description said a long wait costs nothing.

#4235 A step ended by a signal is reported as "was killed" with no signal named. The planner cannot tell an out-of-memory kill from a crash.

#4231 File tool refusals outside the workspace name /add-dir and a file drop in a run where nobody can be asked. Only the run redirection refusal was worded for that case.

#4236 vet_content refuses a file that a listing found beside the workspace under bypass, where read_file shows it. The refusal also offered /add-dir.

#4240 Enter mid-turn on a line that begins with ! sent the line to the model as words. Shell mode cannot be entered during a turn.

#4238 A run result sent to the planner keeps terminal colour codes that the person's own view strips. Left: the Verus contract row cannot run because verus is not installed on the host, and make check-verus must run in CI.

#4234 The /mcp reference page does not mention remove, the permanent form or the * pattern. An answer from the bundled help said the command may not exist.

#4237 One refused Bedrock request switches cache breakpoints off for the model for the rest of the session, with no notice. Every later request pays full price for the prefix.

#4239 A delegate that ends as a transport failure is reported with no cause, so the planner starts the same task again. Left: handing back a failed delegate's work (R21) needs a design decision under DELEGATE-9, which lets only a report cross back. The decision is whether a failed delegate leaves a partial account that crosses with the label its context earned.

Reproduce

#4230 opener test that waits on the installed xdg-open

Nothing a person sees changes, so the test is the reproduction.

  1. Run BRAVEBOT_ALLOW_UNCONFIGURED_BUILD=1 cargo test -p bravebot-tui --lib opener:: --locked.
  2. The test an_opener_that_does_not_end_fails_the_test_and_is_killed_with_what_it_started passes a stand-in xdg-open to handler_run_by_within. The stand-in starts (sleep 3; : > survived) in the background and then runs exec sleep 600.

Before the change the call waits until the sleep ends. After it, with a 1 second deadline, the call panics in about 1 second naming XDG_DATA_HOME, and survived is never written.

Not reproducible locally: the hang itself needs a Linux host with xdg-open and a desktop session. This host is macOS, and the tests that run the real xdg-open print skipped.

#4169 stage stopped by the terminal
  1. In a macOS terminal in a trusted directory, start HOME=<scratch dir> bravebot.
  2. Ask the model to start a background run of python3 -c "import os,termios; fd=os.open('/dev/tty', os.O_RDWR); termios.tcsetattr(fd, termios.TCSADRAIN, termios.tcgetattr(fd)); print('done')".
  3. Ask for job_output with wait_seconds 10, and again later. In another terminal run ps -o pid,stat,command.

Today the program never prints done, every look says it is still running, and the state is T. After the change opening /dev/tty fails, the program exits non-zero, and job_output says so. A stage stopped some other way (kill -STOP $$) is reported as stopped by the signal and ended.

Not reproduced: the hang through the terminal client. The pty harness started a Python stage that opens /dev/tty and calls tcsetattr. Started as a process group only, it was stopped by SIGTTOU and ps showed T. Started in a session of its own, it exited with code 1 and OSError: [Errno 6] Device not configured: '/dev/tty'.

#4233 job_output wait longer than the cache lifetime

A live backend that reports cache figures is needed to see the cost, so the tests are the reproduction.

  1. Leave promptCacheTtl unset and ask the planner for a background run of sleep 600.
  2. The planner calls job_output with until_exit: true and wait_seconds: 600.

Today the call is accepted, and the next request writes the whole prefix again and reads none. After the change it is refused with a message naming 270 and the cache reason, and wait_seconds: 270 is accepted. With promptCacheTtl set to 1h the bound is 600. tools::the_longest_wait_follows_the_cache_lifetime_chosen covers it.

Not reproducible locally: the recorded session behind the issue (25 waits over 300 s) is not in the repository, and no live backend run was made.

#4235 step ended by a signal
  1. Start the terminal client in a trusted scratch directory with HOME redirected, against a stand-in model server (contrib/README.md).
  2. The stand-in calls run with background: true on sh killme.sh, where killme.sh holds kill -9 $$, then calls job_output. Repeat with kill -15 $$ and kill -11 $$.

Today each result reads It failed: step 1 was killed. After the change they read step 1 was ended by signal 9 (SIGKILL), signal 15 (SIGTERM) and signal 11 (SIGSEGV). A foreground run of the same line says the same. a_job_output_call_names_the_signal_that_ended_a_finished_job in crates/agent/tests/turn.rs covers the three.

Not run: Linux and Windows. The signal names on Linux are reasoned from rustix, and the Windows build is reasoned only.

#4231 refusal outside the workspace in an unattended run
  1. Use HOME=$(mktemp -d). Make proj/ and outside/todo.txt beside it.
  2. In proj/ run bravebot "read ../outside/todo.txt" (a one-shot, so nobody can be asked), with the model scripted by the turn-test server to call read_file on the absolute path and list_files on outside/.

Today the refusal says the person can open the directory with /add-dir or --add-dir, or drop the file. After the change it says nobody can be asked in this run and to say in the reply which path the work needs and what for, and the trail records remedy offered: none. In an attended session the wording is unchanged. a_read_outside_the_workspace_nobody_can_be_asked_in_names_no_persons_route and an_unattended_turn_refused_outside_by_read_file_and_by_run_is_given_one_answer cover it.

The three new tests fail on upstream/main code, and the old wording put back fails the turn test.

#4236 reference to a listed file under bypass
  1. Make $TMP/ws and a sibling $TMP/side/notes.txt. Start bravebot --dangerously-skip-permissions in $TMP/ws with HOME redirected.
  2. Script the model with the turn-test harness: list_files on $TMP/side, then vet_content on the reference it returns.

Today vet_content is refused with ref:1 could not be read: 'ref:1' resolves outside the workspace and the /add-dir advice, while read_file on the same path is shown. After the change the reference opens. In Ask, AcceptEdits and Plan modes the listing itself is refused as before, and a reference made under bypass is refused once the mode leaves bypass. under_bypass_a_reference_to_a_listed_file_beside_the_session_is_opened failed with the refusal above on the unmodified tree.

#4240 a ! line mid-turn
  1. Start the terminal client in a trusted directory and send a message that keeps a turn running.
  2. While it runs, type !!pwd and press Enter, or Ctrl-Enter.

Today the line leaves the box and is queued or interjected to the model. After the change the line stays in the box with a note that shell mode is for when the turn is idle, and nothing is queued. A line with ! after its first character is still queued. a_bang_line_pressed_mid_turn_is_held_and_never_interjected and ctrl_enter_on_a_bang_line_mid_turn_neither_queues_it_nor_stops_the_turn cover it.

Other choices: a pasted ! line is held the same way, leading white space counts, the note is a transcript note and not a hint line, the French text is a new translation, and a ! line while an enhancement is pending is still queued as an enhanced prompt (INPUT-48).

Not run live: drive_tui.py needs a stand-in server that keeps a turn open, and none was available.

#4238 colour codes in a run result sent to the planner
  1. Use a throwaway HOME and a trusted throwaway directory.
  2. Start the terminal client against a stand-in model server (contrib/README.md). Its first reply calls run with {"command":"printf '\\033[0m\\033[36mok\\033[0m done\\n'","read":true} and its second reply is done.
  3. Look at the tool message for the run in the second request the stand-in receives.

Today the text holds ESC [ 0 m ESC [ 3 6 m ok ESC [ 0 m done byte for byte. After the change it holds ok done followed by 3 colour codes were taken out of this output. A result with no colour code gets no sentence, and other escape sequences are left alone. cargo test -p bravebot-agent --test turn --locked -- colour covers each route.

Not run live: the drive_tui.py form. The in-tree stand-in server in crates/agent/tests/turn.rs was the reproduction, and the tests were not run against the parent commit.

#4234 /mcp reference page
  1. Ask the bundled help (reference/commands) whether /mcp remove exists, or read the /mcp row and section of docs/website/docs/reference/commands.md.

Today the row and heading list list, reconnect and disable only. After the change they list remove <alias>, the section describes the permanent form and the * pattern, and the slash-menu hint reads [list | reconnect | disable | remove]. help::tests::the_mcp_section_of_the_commands_page_names_every_form_of_the_command fails on the previous page, and command_table::tests::the_mcp_hint_names_every_subcommand_the_usage_message_names failed on the previous hint.

#4237 one refused Bedrock request switches breakpoints off
  1. Configure a Bedrock tier and point its endpoint at a stand-in server that answers the first request with HTTP 403 and every later request with a normal Converse reply.
  2. Send three prompts, then open /status and the info panel.

Today the second request is answered without breakpoints, the model is recorded as refusing them, and every later request carries none, with no notice. After the change breakpoints are recorded as refused only after a second refusal on a request that began later, the record lapses after 30 minutes, and /status and the info panel say when breakpoints were given up. one_refused_request_does_not_stop_later_requests_carrying_the_breakpoints covers it.

Not run live: the tests use scripted responses in crates/bedrock/src/lib.rs.

#4239 delegate ended transport or incomplete

The failure needs a model service that goes quiet, so the tests are the reproduction.

  1. Start the terminal client with a throwaway HOME against a stand-in server that answers the planner with a spawn_agent call.
  2. For the delegate's request the stand-in hangs up (transport) or sends two stream events and no finish (incomplete).

Today the planner message is The delegate d1 did not finish. It ran N commands and made N writes. After the change it also says the service sent no refusal, that starting the same task again may meet the same silence or stop the same way, and to split the task. No text from the service is carried. a_delegate_that_ended_on_a_limit_an_outage_a_silence_or_a_stopped_reply_is_told_to_the_planner covers both.

The fix

#4230. The opener tests wait for the installed xdg-open or gio at most 20 seconds, then kill it with everything it started and fail naming the variable. One helper in crates/tui/src/testutil.rs does the wait.

#4169. Every unix stage starts in a session of its own, so it has no controlling terminal and opening one fails. A stage still found stopped is sent TERM then CONT, killed after a grace period, and reported once as stopped by the signal. RUN-11, RUN-15 and RUN-17 say so.

#4233. Deadlines carries the longest wait for the cache lifetime in force, wait_from refuses a longer value naming the bound, and the tool descriptions state the cache cost.

#4235. failed_steps and how_it_ended take the signal list recorded for each stage and name the signal, with a name table that resolves per platform through rustix.

#4231. Policy records that nobody can be asked, and the shared wording for every file tool maps each open and drop remedy to no route and says to name the path in the reply. TRUST-10, TRACE-1, CMDLINE-5 and PATHREQ-7 say the same.

#4236. A second table lists the tools that open a reference (vet_content, spawn_processor, read_output), and materialise holds a read reach for them under bypass only.

#4240. A new arm in handle_key_while_working holds a ! line in the box with a note in English and French. SHELL-8 is new.

#4238. Policy::present_output takes complete colour codes out of the text of a trusted run, job_output or read_output result and says how many, on every route. without moved from bravebot-tui to bravebot-core.

#4234. The row, heading and section list remove <alias> and the pattern, the slash-menu hint names every subcommand, and CMD-6 lists the new test.

#4237. Bedrock holds a first refusal as a doubt, records it only when a later request is refused too, lets it lapse after 30 minutes, and draws a notice in /status and the info panel.

#4239. unfinished_account names the transport and incomplete endings with the advice to split the task, and a Bedrock account reached through the tier variables can state its retry count and idle gap.

Test plan

Failures that also occur on upstream/main

render::tests::todos::a_cancelled_task_is_struck_without_the_finished_colour and render::sessions_screen::tests::a_calm_header_says_so_and_only_the_waiting_count_has_a_colour fail intermittently in bravebot-tui on upstream/main too, because neither takes theme::exclusive(). No issue or pull request is cited yet: gh issue list and gh pr list --repo brave/bravebot --search "theme exclusive colour test flaky" --state all found none.

  • CI passes cleanly

…0 seconds and kills it with what it started

The generic-mode opener test ran for 16 and 48 minutes in two runs because handler_run_by and the child it starts waited for xdg-open with no bound, and the run ended only when the child was killed by hand. A program that has not ended at the deadline now fails the test with the variable under test in the message, on every host, and is not skipped. The program runs in a process group of its own and the whole group is killed, so xprop, xdg-mime and a handler that xdg-open started do not outlive the test.

One helper pair in crate::testutil, status_within and output_within, does the wait. The opener tests use it for the child of handler_run_by (20 seconds, settable by the test through BRAVEBOT_TEST_BROWSER_DEADLINE_MS), for every run of this test binary that starts the installed opener (60 seconds, which covers the child's 20 and its start), for the installed xdg-open and gio runs in the stand-in, KDE and HOME tests, and for the kde-open run in the KDE marker test, which kept its own 30 second loop that killed only the direct child. xdg-open ends in well under a second where it works, so 20 seconds is a failure only on a host that cannot run the test. The waits that are left are the ones in tests that run only on Windows, which the Unix process-group helper cannot serve, and the runs of a compiler, a shell syntax check or a script that the tests write themselves. The cause of the hang is not observed on this host: no host with xdg-open and a desktop was available.

an_opener_that_does_not_end_fails_the_test_and_is_killed_with_what_it_started and a_command_that_does_not_end_is_killed_at_the_deadline_and_one_that_ends_is_not run the deadline against a stand-in that never ends. Each waits on a thread for at most 30 seconds, so a run with no deadline fails and does not hang. docs/specs/sandboxing.md SANDBOX-30 states the deadline and lists both tests under verified-by.

Part of brave#4230
…stopped by a signal

A stage on macOS, and an unconfined stage on Linux, led a process group of its own and kept the controlling terminal. A program that opened or configured the terminal was stopped by SIGTTIN or SIGTTOU, and a background job has no deadline, so job_output reported it as running for as long as it was asked.

Every stage on unix is now started as a session leader through one helper in bravebot-sandbox, called by the Seatbelt backend and by the unconfined start. A program that opens /dev/tty gets an error instead of a stop. A stage is started with a session or a group and never both, since setsid fails in a process that already leads a group.

A stage whose leader is found stopped by waitid, without collecting its exit, is ended with SIGTERM then SIGCONT and reported as stopped by the signal, by the foreground wait and by job_output, once, and the turn's look between rounds does not report it again. The check reads the kernel's state for a child the driver started and nothing the job printed. RUN-11, RUN-15 and RUN-17, the known costs and the security page say so.

Closes brave#4169
The longest wait_seconds is now 270 seconds where promptCacheTtl is 5m or
unset and 600 seconds where it is 1h. A longer value is still refused
rather than shortened, and the refusal names the bound and the reason.

A request sent after the cached prefix expired writes the whole
conversation again and reads none of it. Waits of 370 to 595 seconds
made each following request do that, which the previous bound of 600
allowed and the description encouraged by saying a long wait costs
nothing.

Deadlines carries the bound as wait_ceiling, resolved from the
settings by the caller that read them (Deadlines::from_settings), so the
tool compares only the argument the planner typed against a figure the
person set. The descriptions of wait_seconds, deadline_seconds and
job_output quote the bound in force and state the cache cost. A foreground
run deadline is not bounded, and its description points at background
runs watched in shorter waits. RUN-17, BACKEND-27, the Known costs entry
for cache expiry, terminal-input and the website pages state the new
bound.

Closes brave#4233
A step a signal ended was told as "step N was killed", so a planner could not tell a program the kernel killed for memory (SIGKILL) from one asked to end (SIGTERM) or one that crashed (SIGSEGV), and restarted a long gate without knowing which. The exec layer already records the signal of each stage beside its code. failed_steps and how_it_ended now take that list and say "step N was ended by signal 9 (SIGKILL)". The name is looked up from rustix's per-platform Signal values, so Linux and macOS numbers both resolve, and a signal with no name here is told by its number alone. A stage with neither a code nor a signal, which is one whose status could not be read, is told as "step N ended without an exit code", since no signal was read. A command substitution a signal ended is refused with the same words. The words are fixed and the number is the one the system reported, so no byte a program printed reaches the sentence. RUN-13, WATCH-17 and the two website pages say the signal is named.

Closes brave#4235
A read, list, search, write or edit of a path outside the workspace was
refused with a remedy naming /add-dir, --add-dir or a file drop in every
run, including a one-shot command or a pipeline where nobody is there to
type or drop. Only a run line's redirection was worded for that case.

Policy now records that nobody can be asked, set by the turn from the
permission mode. The wording moves from the redirection path into the
shared refusal wording used by every file tool and by deferred reads, so
each of them gives a remedy that opens a directory or drops a file as the
remedy that names none, says nobody can be asked in this run and asks the
planner to say which path the work needs. The trail records the remedy the
planner was told. A credential location keeps its meaning and loses only
the drop. The redirection-only helper is removed.

TRUST-10, TRACE-1, CMDLINE-5, PATHREQ-7 and the website pages for tools,
trust and the audit trail say the same, and TRUST-10 lists the new tests
that verify it.

The harm is a planner told to use a route that does not exist. A unit test
cannot measure that, because it would need a planner that follows the
sentence. The tests assert the sentence instead: what a refusal says in a
run nobody can be asked in, for each file tool, for a run redirection, and
for one unattended turn that makes an outside read_file and then an outside
redirection. Each asserts that no refusal names /add-dir, --add-dir, a file
drop, a checkout route or request_path, and that each says nobody can be
asked and asks for the path. The attended controls still name /add-dir.

Closes brave#4231
…le has

Under bypass, vet_content, spawn_processor, read_output, run with stdin_ref and write_file with contents_ref open a file a listing or a search found through tools::materialise. The reach held for the call did not cover that opening, so a file in a directory beside the workspace was refused as outside the workspace and the refusal named /add-dir, where read_file on the same path was shown. In every other mode the refusal is unchanged.

TOOLS_READING_A_REFERENCE gives vet_content, spawn_processor and read_output a read reach under bypass. materialise judges the deferred read as a read through Workspace::reading_under_the_reach, then gives the call its own reach back. Where no reach is held it adds none. The slot keeps its label. PATHREQ-7 and MODE-4 describe the rule and the new tests are listed as verified-by.

The issue's alternative, a quarantined-listing sentence that names read_file with the path under bypass, is not taken: the reference opens directly, so the listing sentence is unchanged.

read_output does not open a file slot: it refuses one as it does in every mode and names vet_content, and a turn test asserts that under bypass. The reads of a round that run together are given the reach of the live mode, and a turn test asserts that for bypass and for the mode that asks.

Closes brave#4236
The mode is armed at an idle prompt only, so a ! typed while a turn runs is a character in the line. Enter on such a line went through the generic queueing arm, which put the text into the planner's interjection buffer at once. A line written as a command, and a !! line meant to stay private, reached the model as a sentence once for each press, and the model ran the command itself with its own tool.

Enter and Ctrl-Enter on a line whose first character after leading white space is ! now queue nothing and hurry nothing. The line stays in the box and a note says shell mode is available at an idle prompt only. The check sits after the arms for the handoff brief and the enhanced line, so it reads only what the person typed or pasted. The line is not queued as a command because a typed bang and a pasted one are the same characters in the box, and only a keystroke may arm the mode (SHELL-4).

SHELL-8 states the case, INPUT-9 names what the key does mid-turn, and the website page describes it. The note is in the en-US and fr catalogs.

Closes brave#4240
A run result, a job_output look, the account of an ended job and a read_output page or search gave the planner every colour code a program wrote, while the screen already counted them and left them out (VIEW-30). Each complete code is now taken out of the text of that output once the cap has cut it, and a sentence says how many were, only where there were any. Every other escape is left as it is.

The removal moves to bravebot_core::colour_codes so the agent and the terminal client use one definition. Policy::present_output makes it from the label alone, so output nobody vouched for is neither changed nor counted. The slot keeps the raw bytes, so the offsets in a cut marker, a page trailer and a matching line still name the slot.

Part of brave#4238
…ce page

The /mcp row, heading and section of the commands reference listed only list, reconnect and disable, so the bundled help page the model reads from reference/commands gave no sign that /mcp remove exists. An answer from it said the command may not exist.

The row and heading now list remove <alias>. The section says remove is the permanent form (runs bravebot mcp disable then bravebot mcp remove, stops the server in this session, refused in an incognito session or with no state directory) and that disable and remove take a pattern with a *, as SERVERS-19 and the usage string say. The * pattern is built at this base (e9ef845) and is documented here.

A test reads the /mcp section of the bundled commands page and checks the heading forms and the remove and pattern sentences. It fails on the previous page text.

The slash-menu argument hint of /mcp in command_table.rs omitted list. It is now [list | reconnect | disable | remove], which names each subcommand in the order the usage message and the commands page give them and keeps the short form the other hints use, without the alias placeholders. A test in command_table.rs pins the hint and its order against the mcp-command-usage message, and CMD-6 lists it. It fails on the previous hint.

Closes brave#4234
…, for thirty minutes, and says so

The status that refuses the breakpoints is also the one an expired credential or a burst of requests gets. The first answered probe recorded the model for the life of the process, so one transient 403 removed the breakpoints from every later request with nothing on screen saying so, and a prefix was read at the full rate each turn.

A first refusal is now held as a doubt, and the model is recorded only when a request that began after it is refused too. Requests that began together count once. The record lapses after thirty minutes, and a request that carries the breakpoints and the lifetime and is answered clears the doubt. The status report and the info panel say that the cache is off for the model while it is recorded, from a fixed catalog sentence, since no cache figure is drawn for it. The gateway and anthropic clients keep remembering on the answer; the specs say so.

Closes brave#4237
…d to split the task

A delegate whose requests went quiet, did not get through, or stopped part way ended with the planner told only that it did not finish. The planner read that as bad luck and started the same delegate again on the same task, which met the same ending. The rate-limited and unavailable endings already had a sentence for this.

unfinished_account now writes a fixed sentence for transport and one for incomplete, chosen by comparing the closed category name the trail records, so nothing the service sent reaches the planner. Each says the service sent no refusal, that starting the same delegate again on the same task may meet the same ending, and that the task should be split so each delegate makes smaller replies or writes. Both compose with the spent-bound sentence. Every other category, a stop and a lost thread are told as before.

DELEGATE-26, BACKEND-37, TRACE-8 and the audit trail page name the four categories. BACKEND-37 says which endings are transport and incomplete, and that a Bedrock reply that never began is not sent again and is told as transport. The integration test stand-in can now hang up or send a stream with no end.

The Bedrock test for the doubt cleared by an answered request now also checks that a later single refusal is not counted with the cleared one. Replacing the clearing with nothing left the old test passing. The session test for a rate limit past the longest wait now covers an account reached through the tier variables too; removing the ceiling guard on that arm left the old test passing.

Not built: a setting for the idle gap and retries on the tier-variable path, and handing back the work a failed delegate had done. DELEGATE-9 says nothing but the report crosses back, and a delegate that failed has no report, so handing back that work needs DELEGATE-9 amended first.

Part of brave#4239
@netzenbot netzenbot self-assigned this Nov 15, 2026
Comment thread docs/specs/tools/read-output.md Resolved
@netzenbot-reviewer netzenbot-reviewer added pr/risk/guarantee Can let untrusted content decide something, or changes what trust or egress allow pr/risk/network-calls Adds or alters a network request or destination labels Nov 15, 2026
…ndex is a char boundary

The lint-increase check counts clippy::string_slice sites in core. The slices in colour_codes::without start at an ESC found by find, end at a run of ASCII parameters, or sit one byte past an ASCII m, so each is a char boundary.

Part of brave#4238
Comment thread crates/core/src/colour_codes.rs Resolved
…d file

read_output refuses a reference to a file and opens none (OUTPUT-2), so it is removed from TOOLS_READING_A_REFERENCE and from the lists of tools that open a reference in request-path.md and permission-modes.md. The reach test now asserts that reach_for returns None for read_output under bypass.

Part of brave#4236
code_end reads only offsets inside the bytes it is given, and any offset it returns is the m that ends a colour code, at least two bytes in and no more than the parameter limit past them. The slices in without cut the text at the start of an escape or one byte after that m.

Part of brave#4238
Comment thread docs/specs/tools/request-path.md Resolved
…ile tool's refusal does

The sentence states what the refusal names and points at TRUST-10 for the case where it names none, instead of comparing with an earlier version.

Part of brave#4236
@bbondy
bbondy enabled auto-merge November 15, 2026 21:06
@bbondy
bbondy added this pull request to the merge queue Nov 15, 2026
Merged via the queue into brave:main with commit df17ceb Nov 15, 2026
28 of 30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment