You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
September 20, 2026 (v3.5.88): Autonomous by default, /resume by name, and transcripts you can read.A request becomes a result — /auto carries a task end to end, and /resume lists sessions by name.(1) Autonomous mode, and it is now the default. The permission prompt was doing its job on the wrong axis. A turn that needs six commands stopped six times, and the answers didn't accumulate: approving Bash:rm with s lasted exactly one session, so the same keystroke came back tomorrow, and the only way out was accept-all — which is deliberately session-only and has to be re-chosen every launch. Ask a simple question and you'd get an approval menu instead of an answer (Allow: Run: date; date -u; TZ=Asia/Shanghai date, six options, for reading the clock). So auto_approve now ships as True: a request is carried out end to end and you get the result. /auto off turns the prompts back on and is saved, /auto / /status report the state, --ask (alias --no-auto) enables prompting for one run and --auto (alias --yolo) forces autonomy for one run without touching your saved preference. The startup banner stays quiet on the default and calls out the other state — auto (asks before acting) — since that's the one worth a line. Two answers were also added to the prompt itself, for people who run with /auto off, and both stick: ! saves the grant for one signature (Bash:rm, Edit:/repo/app.py) to the always_allow list in config.json, so a command you have already vouched for is never asked about again in any future session; auto switches to autonomous mode from the prompt. What autonomy does not bypass: the Bash hard denylist (host-destroying commands are refused below the permission gate, so "approved" still doesn't mean "runs"), the filesystem sandbox and credential-path denylist, and the manual / plan permission modes — those are explicit "ask me" / "touch nothing" requests, and a saved convenience must not silently undo them. The launch flag writes to a _-prefixed session key rather than auto_approve, so an unrelated save_config later in the run (from /model, say) can't leak one-off autonomy into every future session. /permissions lists saved grants next to session ones, /permissions allow <sig> adds them, /permissions forget <sig|all> drops them. (2) /resume shows session names. It used to load session_latest.json and nothing else; /load offered a menu, but its rows read 14:32 id:4f2a1c turns:12 session_143207_4f2a1c.json — an id tells you nothing about which conversation it was. Sessions now carry a title derived from their opening request (first real user message; slash commands, tool results and framework-injected turns are skipped, newlines collapsed, ellipsized to one line), saved into the session JSON and the SQLite row. /resume with no arguments lists recent sessions newest-first — "Add a /resume picker with session names" · 2026-09-19 14:32 · 12 turns · id:4f2a1c — with the live autosave pinned at the top and marked ← last, deduped across the autosave, the daily backups and the database, and a corrupted file is skipped rather than fatal. Answer with a row number or an id; /resume last, /resume <id> and /resume <path> skip the picker; a non-interactive run (-p, piped stdin, cron) has nobody to answer a menu, so it keeps the old behavior and resumes the latest directly. 49 new tests. Fix: a changed default could never reach an existing user.save_config wrote every key, defaults included — so the first time anything saved (picking a model, answering ! at a prompt) all ~76 default values were frozen into config.json. Changing a default in a later release then reached new installs only: v3.5.88 shipped autonomous-by-default and existing users still got Permissions: auto (asks before acting) and the six-option approval menu, because their file said auto_approve: false — a value nobody had chosen. save_config now persists only what differs from DEFAULTS, which loads identically (the saved file has always been layered over DEFAULTS) while letting defaults actually behave like defaults, and a one-time config migration cleans up the frozen values already on disk (a real 76-key file drops to 22 — API keys, model, bridge credentials, budgets; nothing a user set is touched). The migration is recorded with a config_version marker that is deliberately not a DEFAULTS key, since the new pruning would otherwise erase the marker and re-run every migration on the next launch — which would have silently undone a deliberate /auto off. Also fixed: permission signatures named a shell fragment, not a program.shlex.split keeps operators glued to the token they touch, so date; date -u; TZ=Asia/Shanghai date produced the signature Bash:date; — meaning the grant you saved with ! matched nothing you would ever run again, and the same command prompted forever. Operators are trimmed and a leading VAR=value assignment is skipped, so that command signs as Bash:date and curl -s a ; echo ---; curl -s b as Bash:curl; the migration also drops the dead operator-bearing grants already saved. 13 new tests. The stray horizontal line mid-turn was the model's ---, and it is now off by default. Models separate sections with a Markdown thematic break, and Rich paints that as a full-width rule. Because the turn summary prints at every tool round-trip, the rule landed between two summary lines (Fetched 1 URL, ran 1 shell command … rule … Ran 2 shell commands), where it reads like the app drew a divider rather than the answer containing one — the blank line around it already did the separating. Breaks are now stripped before rendering; /config markdown_rules=true brings them back. The stripping is position-aware rather than a blanket delete of --- lines, because the same characters mean different things: a bare - run directly under a paragraph is a setext H2 underline (deleting it silently demotes a heading to body text), a | --- | --- | row is a table separator, - item is a list, and a --- inside a fenced block (``` or ~~~, including an unterminated one from a truncated stream) is content, such as YAML front matter in a code sample. All four survive; only a real thematic break — ---, `***`, `___`, `- - -` — is dropped. 22 new tests. `/resume` now brings the conversation back, and transcripts are stored as text.(1) A resumed session looks resumed. `/resume` restored `state.messages` and printed one line about where the file was — so the model had the whole history and the user had an empty screen, talking to something that remembered a conversation they could not see. The transcript is now repainted: user turns in the prompt's own styling, assistant replies through the normal Markdown renderer, and each batch of tool calls collapsed to the same one-line summary a live turn prints (`Fetched 1 URL, ran 1 shell command`). Order follows what actually happened — an assistant message that says "let me check" and calls a tool prints the sentence above the summary it announces, not below it — and a `tool_result` message is plumbing, so it is not replayed as a second user turn. `resume_replay_limit` (default 40) caps how much is painted and says how many messages were elided; `resume_replay=false` restores silently. Replay is skipped on a Telegram / Slack / WeChat / QQ turn, where a whole transcript would arrive as one enormous chat message and the user is scrolling that history in the chat app anyway, and a rendering failure warns instead of costing the restored session. (2) Sessions are saved as UTF-8, not `\uXXXX`. Every `json.dumps` on the session path ran with the default `ensure_ascii=True`, so a Chinese message was stored as `"\u5e2e\u6211\u770b\u770b..."` — valid JSON, unreadable to a human opening the file, and hostile to `grep`. `ensure_ascii=False` now applies to the session files, the daily backups, `history.json`, the SQLite message blob, the `/cloudsave` Gist copy and `config.json`; reads and writes on those paths also pin `encoding="utf-8"` explicitly, since a C/POSIX locale would otherwise raise `UnicodeEncodeError` on the first non-ASCII character now that one can reach the file. Older escaped files keep loading unchanged — the escaping was never a content difference. 12 new tests. 143 new tests across the five areas. Details