llama-atmosphere-agent: REPL commands, approval prompt, status line, rendered answers - #454
Open
bernardladenthin wants to merge 35 commits into
Open
bernardladenthin wants to merge 35 commits into
bernardladenthin wants to merge 35 commits into
Conversation
…rendered answers
Four things a terminal agent needs to be usable, built on what Atmosphere already
has and without a new dependency:
- SlashCommands: /help /status /tools /mode /compact /clear /exit, with aliases and
a trailing argument. An unknown /command goes to the model rather than being
rejected, so no escape syntax is needed for a line like /usr/bin/env.
- Approval: ConsoleApprovalStrategy asks [y]es/[n]o/[a]uto before run_command,
write_file, edit_file, delete and rename; reading tools never ask. The gate is
Atmosphere's (ToolApprovalPolicy.custom + ApprovalStrategy, attached by
AgentRunner.approval), so a denial reaches the model as its own
{"status":"cancelled"} tool result and it replans. Manual is the default; --auto
and the [a] answer switch the session to auto. One-shot (--prompt) has nobody to
ask, so a gated call is denied unless --auto was passed.
- /compact summarizes the conversation through the same model with no tools
(compact-prompt.txt) and replaces the history with the summary as a user message
plus a short acknowledgement.
- Status line above the prompt: [manual · ctx ~3.1k/16k · 9 tools · local-model].
llama.cpp reports usage only to clients that ask (stream_options.include_usage)
and Atmosphere's does not, so the count is normally an estimate, marked with ~;
the window size comes from --ctx-size or the server's /props (ServerProps).
- Console: answers are rendered line by line as they stream (headings, bullets,
fences, inline bold/code) and tool calls print as "● tool {args}". Append-only,
never redrawn, so piping stays correct. Colour is decided once (CLICOLOR_FORCE,
NO_COLOR, TERM=dumb, CLICOLOR, then "is a terminal").
Tests: 61 green. ApprovalWireTest drives denial and approval through the real
OpenAiCompatServer and asserts what reaches the model; ConsoleApprovalStrategyTest,
SlashCommandsTest, ConsoleFormattingTest and ServerPropsTest cover the rest.
Verified live with Qwen3-4B on Vulkan: /help and /status are intercepted, a denied
"rm -rf build" is reported to the model, /mode auto takes effect, answers render.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
bernardladenthin
had a problem deploying
to
maven-central
September 22, 2026 21:48 — with
GitHub Actions
Failure
bernardladenthin
had a problem deploying
to
maven-central
September 22, 2026 21:48 — with
GitHub Actions
Failure
The new commands, the approval prompt and the rendering are documented feature by feature, but nothing showed what a first session looks like. Adds a short table: /help, /status, /tools, a shell call denied then approved, /mode auto, a Markdown answer, /compact, /exit -- in the order that exercises each piece once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
bernardladenthin
had a problem deploying
to
maven-central
September 22, 2026 22:10 — with
GitHub Actions
Failure
bernardladenthin
had a problem deploying
to
maven-central
September 22, 2026 22:10 — with
GitHub Actions
Failure
…pinned status, single key) The REPL read lines with a BufferedReader: arrow keys produced escape sequences, there was no history, the status line scrolled away with everything else, and every approval answer needed Enter. JLine fixes all four, behind one interface so nothing else has to know: - AgentTerminal with two implementations. JLineTerminal on a real terminal: line editing, history, Tab completion of the command names, a status line (plus a rule) pinned to the bottom via JLine's Status, streamed output through LineReader.printAbove so that block stays put, single-key answers via enterRawMode, Ctrl-C drops the line instead of the session. PlainTerminal otherwise: PrintStream + BufferedReader, no cursor control, correct when the output is a file. JLineTerminal.open returns null (rather than throwing) when there is no usable terminal, and the caller falls back -- which is also why no test needs a TTY. - While a turn runs the status line shows progress: "⠙ working… (12s · 2 tool calls)". A local model can think for a while, and a silent console is indistinguishable from a hung one. - ConsoleSession, MarkdownConsole and ConsoleApprovalStrategy now write lines to the terminal instead of a PrintStream; MarkdownConsole takes a Consumer<String>. One dependency: org.jline:jline 4.4.5, a single jar with no transitive dependencies. Verified on Windows 11 that JLine picks the windows-vtp provider, so the pinned status line, single keys, Tab completion and Ctrl-C all work there. Tests: 66 green (was 61); PlainTerminalTest covers the fallback console and that every command name is offered for completion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
bernardladenthin
had a problem deploying
to
maven-central
September 22, 2026 22:27 — with
GitHub Actions
Failure
bernardladenthin
had a problem deploying
to
maven-central
September 22, 2026 22:27 — with
GitHub Actions
Failure
…file /loop [--every 5m] [--max 20] [--check '<cmd>'] <task> works on one task step by step without the user typing between steps. - Every step sends the same message: the task verbatim plus "read AGENT-LOOP.md, do one concrete step, write down what happened". The history is dropped between steps, so the file is the memory and the context cannot grow (the shape Claude Code's ralph-wiggum plugin uses). - The loop ends when a line of the answer is exactly <<TASK_COMPLETE>>. A text marker, not a "done" tool: below 7B a model emits a malformed tool call far more often than a malformed line, and mini-SWE-agent's SWE-bench results come from a plain sentinel with no tool-call API at all. A substring never counts, so the model talking about the marker cannot end the run. - --check '<cmd>' only believes the marker when that command succeeds, and feeds its output back otherwise -- the cheapest defence against a 4B declaring victory. - Guards, all enforced here: step cap (20), two-hour budget, stall detection (three steps with no file change and no tool call), optional interval. A loop asks once to switch to the auto approval mode and gives up if the answer is no. The marker is checked BEFORE the stall detector: the step that only answers "done" changes no file and calls no tool, and the other order reported it as "no progress" -- caught by theLoopEndsWhenTheModelAnswersWithTheMarker while writing the tests. Also: the workspace is now part of the pinned status line, shortened to its last two segments when the path is long. Tests: 81 green (was 67). TaskLoopTest drives the loop over the real OpenAiCompatServer with scripted answers: marker, empty history per step, stall, step limit, failing check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
bernardladenthin
had a problem deploying
to
maven-central
September 22, 2026 22:46 — with
GitHub Actions
Failure
bernardladenthin
had a problem deploying
to
maven-central
September 22, 2026 22:46 — with
GitHub Actions
Failure
Three of Atmosphere's file tools are replaced (not added alongside — two tools that both read a file is the worst case for tool selection). They call the same AgentFileSystem, so confinement, path validation and size limits are unchanged; what changes is the parameters the model sees and the text it gets back. edit_file (TextEdits): - Normalizes line endings before matching and restores the file's own ending and BOM. The framework matches raw content, so on Windows a model's LF text never matches a CRLF file and every edit fails silently. - A miss shows the closest lines with their numbers instead of "not found"; an ambiguous match names the lines and offers replace_all. A failed edit is not a free retry: on SWE-agent trajectories the eventual success rate drops from 90.5% to 57.2% after one failure. - edits[] applies several replacements all-or-nothing. - Refuses to edit a file that was not read in this session (ReadTracker), so old_string comes from the file rather than from memory. grep (WorkspaceSearch): the framework walks alphabetically with one global 2s deadline and one global 500-hit budget, so .git, target and node_modules consume both before src is reached. This one excludes them, groups by file with line numbers, caps at 100 matches and says when it truncated. read_file: offset/limit and numbered lines; whole-file reads measure 12.7% vs 18.0% task success in the SWE-agent ablations. The numbers are display only, which the tool description and the system prompt both say. Rejected on evidence (see CLAUDE.md): unified diff/patch, fuzzy matching, an embedding index, LSP tools. Tests: 101 green (was 81). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 08:07 — with
GitHub Actions
Failure
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 08:07 — with
GitHub Actions
Failure
A real session showed the failure this fixes: after three turns a 4B model stopped calling tools and started describing the work instead -- it reported JUnit tests, a Maven build and a .bat script it had run, with exit codes, while the workspace contained only the two files from turn one. Nothing had been executed, so nothing asked for approval either. The cause is on our side: the history held the user's messages and the model's prose and nothing else, so from turn three the model saw only its own paragraphs and no evidence it had ever used a tool -- and continued that pattern. - LocalAgent.withToolNotes prefixes each turn's answer in the history with "(tools I actually ran this turn: <tool> <args> -> <result cut to 400 chars>)". A text note rather than real tool_calls messages because Atmosphere's AbstractAgentRuntime.assembleMessages rebuilds every history entry as new ChatMessage(role, content) -- the tool-call array and id are dropped, so protocol-faithful replay is impossible through the framework's history. - ToolCallLog + /calls (/log): every call of the session with its result. An empty list is the proof that nothing ran, whatever the prose says. - System prompt: never claim to have run a command, created a file or seen a result without having called the tool. Tests: 102 green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 08:48 — with
GitHub Actions
Failure
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 08:48 — with
GitHub Actions
Failure
…he tool record Two problems from one session. The tool record added in the previous commit rode in front of the assistant's answer -- and the model copied it into its next reply, so the user read "(tools I actually ran this turn:" as the first line of an answer. And a long command showed nothing at all until it finished. - The record now rides in front of the NEXT USER message. An assistant message is text a model imitates; a mid-history system message would be cleaner but Mistral's template requires strict user/assistant alternation and Gemma has no system role, so this is the placement every template accepts. It also says "do not repeat it". - The pinned line names what is happening: "⠙ thinking… (5s)" while the model generates, "⠙ run_command… (47s of 61s · 2 tool calls)" while a tool runs. A two-minute build was indistinguishable from a hang. - run_command prints its output line by line while it runs instead of dumping it at the end. Reading the pipe incrementally also keeps the child from blocking once the pipe buffer is full -- about 4 KB on Windows. Tests: 105 green (was 102). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 10:58 — with
GitHub Actions
Failure
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 10:58 — with
GitHub Actions
Failure
The activity line now draws a word from spinner-words.txt once per turn: "⠙ Fettling… (run_command 47s of 61s · 2 tool calls)". Deliberately our own two dozen words, not Claude Code's: that list is extracted from a proprietary binary, and the public copies are either unlicensed (all rights reserved) or CC BY-NC-SA, which is incompatible with MIT and with REUSE. A test asserts that none of their words appear here, that the list has no duplicates and that every entry is a plain capitalised word. The tool name and the elapsed times stay next to the word: with a local model "which tool, for how long" carries more information than the joke does. Tests: 106 green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
…resources loop-prompt.txt and loop-file-template.md shipped without a .license file, which fails the REUSE Compliance Check on main -- the same mistake as the agent's .mvn/jvm.config, caught this time by running `reuse lint` before pushing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 16:26 — with
GitHub Actions
Failure
Reported as "weird at the bottom when I press Enter a few times", and it was two defects, both mine from the previous commit. The rule above the input was the first line of a two-line prompt, while ERASE_LINE_ON_FINISH erases exactly ONE line -- so every Enter left the rule behind and holding Enter drew a column of them. The rule is now ordinary output (AgentTerminal.separator(), drawn before each read and suppressed when nothing has been printed since, so repeated Enters draw nothing); the prompt is one line again. The look is unchanged: top rule as output, input, then the status block whose own first line is the bottom rule. The stray "[?1h" above the prompt was separate: line() wrote straight to the terminal whenever the reader was not inside readLine, and that instant is exactly when the next read emits its init sequence. The two interleaved and half an escape sequence landed in the scrollback as text. Once the reader thread exists, everything goes through printAbove. JLineTerminalTest is the new part worth keeping: JLineTerminal.over() takes a terminal built over two streams, which renders exactly like a TTY, so the screen can be asserted on the bytes. Two findings that are not guessable and are written down: the test terminal needs stdoutEncoding as well as encoding or every rule arrives as "?", and a box character goes out as UTF-8 from printAbove but as the DEC line-drawing set inside a prompt -- so the first version of the counter passed against the exact bug it was written for. Verified by restoring the two-line prompt and watching it report 5 rules where it now reports 1. 125 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 17:29 — with
GitHub Actions
Failure
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 17:29 — with
GitHub Actions
Failure
…minal The bar filled with escape fragments and then disappeared. Three separate writers, only one of which I had accounted for. llama.cpp logs to stderr, which with an in-process model is the same console, and those writes go around JLine entirely -- the terminal scrolls lines it never sees and its reserved region ends up somewhere else than it believes. That is what "> d1.19.029.542 W srv stop: cancel task" is, and after a few of them the block is gone. captureNativeLog routes the log through LlamaModel.setLogger -- the callback sink patches/0014 added, for exactly this -- into terminal.line. The turn thread (Atmosphere's) prints tool lines while the console thread refreshes the block four times a second; neither printAbove nor Status.update knows about the other. A "writing" lock now spans line() and status(). That is the "1H" that was drawn into the middle of the rule. The rule above the input is gone. Carrying it in the prompt leaves one behind on every Enter; drawing it as output leaves one per turn in the scrollback, which is "the line keeps travelling along". JLine's pinned region is below the prompt and never above it, so those were the only two options and both were wrong. One rule, below the input, has neither problem. Holding Enter also cancelled one turn per keystroke: hasPendingInput() counted blank lines. It ignores them now but leaves them queued, because an empty answer to an approval question means yes. Honest limit: the lock is reasoned, not test-covered. Two attempts to pin it both passed with the lock removed -- including one that sliced every OutputStream write in half -- because PrintWriter already makes a single call atomic and the interleaving happens between calls, inside JLine. Deleted rather than kept: a test that is green either way is worse than none. 125 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
bernardladenthin
had a problem deploying
to
startgate
September 23, 2026 17:49 — with
GitHub Actions
Error
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 17:49 — with
GitHub Actions
Failure
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 17:49 — with
GitHub Actions
Failure
…s there The input looked like an ordinary prompt again after the last change, and only settled into the block after a few turns. That is geometry, not chance: the line reader draws its prompt where the cursor is -- directly after the last thing printed -- while only the status block is pinned to the window. On a half-empty screen the two are far apart, and they meet once enough output has scrolled the cursor down by itself. Pushing the cursor to the last row before the first prompt makes that the state from the beginning. The cost is a screenful of blank lines above the session, which is what a program that wants its input at the bottom without taking over the whole screen has to pay; the alternative is the alternate screen buffer, and that costs the scrollback. 126 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 17:54 — with
GitHub Actions
Failure
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 17:54 — with
GitHub Actions
Failure
…e history AgentTerminal.clearScreen() defaults to nothing (a stream has no screen); JLineTerminal sends the terminal's own clear_screen capability through printAbove, like every other write, then refills the blank rows and redraws the block. /clear wipes the screen too: what is still on it afterwards is a conversation the model no longer has, which reads as if it were still in play. Neither touches the emulator's scrollback. Two things the tests found rather than the reading. getStringCapability returns terminfo SOURCE -- "\E[H\E[2J", with the escape spelled out -- so writing it as it comes prints that text on the screen; Curses.tputs expands it. And Ctrl-L already did all of this before the command existed, bound by JLine's own keymap, so /cls is the second way to do it and not the only one. Both are pinned. 128 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 18:50 — with
GitHub Actions
Failure
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 18:50 — with
GitHub Actions
Failure
After /cls the bar at the bottom was gone. Status.redraw() writes nothing in that situation: it draws what has changed, and a wipe changes nothing about the block's content -- it only takes it off the screen, which the object has no way of knowing. The rendered block is now kept in a field and restored with reset() (forget what is believed to be on screen) plus update(block). Found by writing the test first: it reproduced the empty bottom, and putting redraw() back makes it red again, so it is a regression test rather than a description. The probe also settled what actually happens on the wire -- the clear sequence was written correctly all along, only the block never came back. 129 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
bernardladenthin
had a problem deploying
to
maven-central
September 23, 2026 19:37 — with
GitHub Actions
Failure
…s line Dragging the window narrower drew a row of "> > > > >" across the screen. Status keeps the size it was created with, so its reserved rows stop matching the window, everything below them is drawn in the wrong place, and a prompt redraw lands beside the previous one instead of over it. A WINCH handler now resizes and resets it and re-renders the block from the text it was built from -- never from the rendered rows, which were cut to a width that no longer exists. Both halves are pinned by tests that go red when the handler is reduced to redraw(). The status line is icons and values now: folder, mode glyph, context, tools, and a robot for a model loaded in this process or a globe for one reached over the network. Each is an icon, a space, its value. fit() therefore measures screen COLUMNS rather than characters -- an icon is one character and two columns, and counting characters lets a row come out wider than the window, wrap, and push the pinned block out of place, which is the tearing this console has had twice already through other doors. Removed again: skipping a status write when the block is unchanged. It looked like the fix for the stray "?1h" -- fewer writes, fewer chances to collide with the setup sequence the reader emits inside JLine where no lock of ours reaches. Taking the guard out again left the emitted bytes identical, because JLine already skips an unchanged block. Deleted rather than kept with a comment claiming a benefit it does not have. The "?1h" therefore still has no established cause, and this commit does not claim one. 133 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
I added a WINCH handler for the "> > > > >" row after a resize; it was reported worse, not better. LineReaderImpl.handleSignal already calls Status.resize, and the reader installs its own handler for as long as it is reading -- the whole session -- so mine either never ran or ran in addition, putting a second writer on the terminal from the signal thread while the reader was redrawing. Removed. A probe raising a real resize signal against a pipe-backed terminal shows JLine doing it correctly alone: scroll region reset, rule re-cut to the new width, one prompt. So the remaining report is not reproducible here and this commit does not claim to fix it. Also removed, both because measurement said they did nothing: skipping a status write when the block is unchanged (JLine already does), and re-cutting rows on a width change (Status.resize does). Neither is kept with a comment claiming a benefit it does not have. What is kept and does matter: fit() measures screen columns rather than characters, because an icon is one character and two columns; and restoring the block after a wipe takes reset, then an empty update, then a render from the text the caller gave -- with only the first two, Status emitted a single character where a whole block was missing. 133 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
…he real signal everyResizeDrawsExactlyOnePrompt drives what actually happens on a resize -- a size change plus terminal.raise(WINCH), with the reader sitting in readLine as it does all session -- across shrinking, growing and a changed row count. Each one draws exactly one prompt, run repeatedly to check it is not flaky. That is the finding worth keeping: the leftover "> " row still being reported is not produced by this path. The remaining suspect is the console reflowing its own screen buffer on a resize, which moves lines the program never wrote again and which nothing on this side can reproduce. Written down as open, with /cls and Ctrl-L as the tested way out. A second test asserting "a clear leaves exactly one prompt" was written and dropped: it depends on where the reader thread happens to be, and the two existing clear tests already cover the erase reaching the screen and the block coming back. 134 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
…what stays open The previous commit updated CLAUDE.md but the README edit did not apply, so the two said different things about the same finding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
… test Reported as "after an interrupt it does not carry on by itself". The sequence is now driven through the real OpenAiCompatServer with a scripted backend: a turn is cut short by pending input, and the next turn is then run. It reaches the server, carries the interrupted question in its history, and answers. So the mechanism works, and this commit does not claim to fix the report -- the cause is elsewhere and I would rather say that than ship a guess. Two things the writing of the test settled, both worth keeping. A turn that finishes within one activity tick is never looked at for interruption, which is correct (there is nothing to cut short) but means the scripted backend has to be slowed down or the test proves nothing -- the first version passed while interrupting nothing at all. And a second line typed during the replacement turn stops that one too: that is the design, not a defect, but from the outside it looks exactly like a turn that never started, which is the likeliest way to misread the symptom. 136 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
…ines An additional interface, next to the existing one rather than instead of it. The line-oriented console was already there -- it is what a piped or one-shot run has always used -- but nothing could ask for it on a real terminal. --plain does. It only ever appends: the status is printed as an ordinary line before the prompt and scrolls away with everything else, there is no pinned block and no spinner, and nothing on screen is rewritten. That is what a session needs when it is piped, logged, recorded, or carried by anything that forwards lines rather than a screen. LocalAgent.usesFullTerminal(options, interactive) is now the single place that decides between the two, which is also what made it testable: the rich console needs someone typing AND permission to move the cursor, and --plain withholds the second even when the first is true. Worth saying plainly in the docs, because it is the likely reason to reach for this flag and it is not the right one: a normal SSH session does not need it. A remote terminal reports its size and handles cursor control like a local one. --plain is for the cases where that is not true. The trade is documented as a table rather than left to be discovered: no input while the agent works (PlainTerminal reports nothing pending, so a typed line cannot stop a turn), no history, no Tab completion, no Ctrl-L, no Shift+Tab. 137 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
…ave it Nothing kept a session log before. The model's history has no timestamps and is rewritten by /compact -- a summary replaces the turns -- and ToolCallLog is a hard-cut receipt for tool calls only. So "what did I ask, what came back, when" had no answer. Transcript records user lines, answers, tool calls and session events, stamped, append-only. /save [name] writes it into the workspace; --transcript <file> appends each entry as it is said so a killed session still leaves what it had, and a failed write is swallowed because a record meant to survive a bad ending must not cause one. /compact keeps it and notes that it happened; /clear empties it, since that command means forget this session. A list, not a map keyed by the timestamp: a tool result and the answer after it regularly land in the same millisecond, and a map would keep one and drop the other without saying so. Insertion order already is time order. Tested at both levels: the class (ordering, same-millisecond entries, blank lines dropped, live append, an unwritable path not ending the session) and end to end through the real server, where /save after a /compact still contains the answer and /save after a /clear does not contain the question. 148 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
/retry sends the last question again and removes the answer that came back from the conversation first -- leaving it would show the model what it said last time, and a model that sees its own answer repeats it, which is the opposite of a retry. The question goes with it because the turn adds it back. Only a trailing exchange that really is the one being retried is touched, so a history that was just replaced by a summary, or one that never got an answer, is left alone. Before anything has been asked it says so instead of sending an empty turn. --system-file replaces the system prompt from a file, which is the same as --system without fighting the shell over quoting and newlines. Read at startup, so a wrong path is a usage error immediately rather than a surprise on the first turn -- a prompt long enough to be worth a file is long enough that its absence should not be discovered mid-session. Four tests: the retry really re-asks with the answer gone, an empty retry is refused without sending anything, the file's content arrives as the system message, and a missing file is rejected at parse time. 152 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
A console sends on Enter, so a stack trace or a function pasted into the prompt became several questions. A line that is exactly """ opens a block and the next one closes it; everything between is one message with its newlines. Chosen over a key combination because it works in both consoles, survives a paste (the fences arrive as part of the pasted text) and asks nothing of the terminal. End of input inside an unfinished block ends the session rather than sending half a thought. Also stabilised a test that had started failing intermittently: the clear-screen one cleared while the reader thread was starting the next prompt, which made what JLine emits depend on which got there first. It waits for the reader to settle now -- that test is about the block coming back, not about that race. Run three times to confirm. 154 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
…nversation Save in one session, load in the next: the questions and the answers become messages again, so the model can be asked to carry on rather than to start over. Two decisions worth stating. Only USER and AGENT entries are replayed -- a tool result outside its round is not something a chat template has a place for, and inventing a shape for it would be worse than letting the model call the tool again; tool lines and notes stay in the record. And the parser treats a line without a stamp as a continuation of the entry above it, because an entry is not a line: an answer keeps its newlines when it is written, so reading line by line would turn one answer into several, each nonsense on its own. A file that is not a transcript yields no entries rather than one wrong one -- anything it yielded would be replayed to the model as if it had been said. A path without a directory resolves in the workspace, so /load session.txt finds what /save session.txt wrote. The round trip is tested both ways: the parser on its own (multi-line entries, junk files, the timestamp surviving to the second the file records it in) and end to end across two sessions, where the second request carries the earlier pair before the new question. 159 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The REPL had three commands (
/clear,/exit,/quit) and no way to see or control anything: no overview, no idea how full the context was, and--allow-shellmeant the model ran commands unannounced. Researched first how Claude Code, aider, Codex CLI, Gemini CLI and OpenCode solve this, and what Atmosphere 4.0.70 already provides.What
Commands (
SlashCommands):/help/status/tools/mode [manual|auto]/compact [focus]/clear/exit, with aliases and a trailing argument. An unknown/commandis sent to the model, not rejected — that is what makes/usr/bin/env …work without an escape syntax (aider rejects; for a prose-first REPL this way round is friendlier).Approval (
ConsoleApprovalStrategy,ApprovalMode):manualis the default. Beforerun_command,write_file,edit_file,deleteorrename:Reading tools never ask. The gate is Atmosphere's, not a reimplementation:
AgentRunner.approval(...)attachesToolApprovalPolicy.custom(...)+ the strategy,ToolExecutionHelperblocks the tool loop and turns a denial into the tool result{"status":"cancelled","message":"Action cancelled by user"}, so the model replans.--auto(and the[a]answer) switch the session to auto. One-shot--promptdenies gated calls — nobody can answer there, and an unattended run must not be the most permissive one./compact: summarizes the conversation through the same model with no tools (prompt incompact-prompt.txt), then replaces the history with that summary as ausermessage plus a short acknowledgement (aider's shape — a conversation may not start with two assistant turns).Status line:
[manual · ctx ~3.1k/16k · 9 tools · local-model]. The~is honest: llama.cpp emits its usage chunk only when the client setsstream_options.include_usageand Atmosphere's client does not, so the count is estimated from the text length; a real count is used when one arrives. The window size is--ctx-sizein-process, or the server's/props(ServerProps), and is omitted rather than guessed.Console: answers render line by line as they stream (headings, bullets, fenced blocks, inline
**bold**/`code`), tool calls print as● tool {args}. Append-only, never redrawn — the redraw-per-token approach of the Ink/Bubble-Tea clients is what produces their overdraw bugs and breaks when piped. No dependency added (no Java library renders Markdown incrementally; the only maintained MD→ANSI one on Central drags in the Kotlin stdlib). Colour obeysCLICOLOR_FORCE,NO_COLOR,TERM=dumb,CLICOLOR, else "is a terminal" (Console.isTerminal()reflectively for JDK 22+).~800 lines including javadoc, in seven small classes.
Tests
61 green (was 32).
ApprovalWireTestdrives denial, approval and a non-gated tool through the realOpenAiCompatServerwith scripted llama.cpp chunks and asserts the executor did/did not run and what the model was told. PlusConsoleApprovalStrategyTest(y/n/a, invalid answer, no console, closed stdin, which tools are gated),SlashCommandsTest,ConsoleFormattingTest(colour decision, status line, streaming renderer fed one character at a time),ServerPropsTest.Verified live with Qwen3-4B-Instruct-2507 on Vulkan:
/helpand/statusare intercepted, a deniedrm -rf buildis reported to the model ("Es sieht so aus, als wurde die Aktion durch den Benutzer abgebrochen"),/mode autotakes effect, answers render,ctx ~223/16k.🤖 Generated with Claude Code
https://claude.ai/code/session_01E2h8gXyyE5UeimkL9vQv9G