A Windows file name cannot hold a newline, so the quoting test could not create its directory. The bidi override still exercises the literal-quoting path there.
The model switcher listed every configured source, so a decision wire
(kind typesafe) appeared beside chat models and choosing one failed in the
provider registry as an unknown kind.
GET /models now answers one job: chat by default, answers=decision for the
decision sources, answers=all for the management views. The switch, the
default model and every role refuse a model whose wire does not answer that
job with a typed code (model.decision_only, model.not_decision_source), and
the provider builder refuses it with config.AnswersMismatchError. A saved
default that names a decision source is passed over like a stale one.
the contract translation (disabled -> none/low, enabled -> default) now
applies only when the menu is the contract, the same rule
InheritedEffortOfficial already used. Before, the Token Rhythm preset shape
sent thinking=enabled with reasoning_effort=max for a stored disabled.
key and hold_key need only the modal their keys go to, so they no longer
read the focused element's value. Typing is confirmed only when the
read-back differs from the value before by exactly the typed text put in at
one place, replacing what was selected there; the changed span is taken
from either end, since repeated characters make it ambiguous. A value that
merely changed and contains a short typed text, as autocomplete or another
writer can leave it, is unverifiable.
A modal's title and the name of the window it holds come from the
application. They were quoted but not bounded, so a 100000-character title
reached the model as about 100 KB, while every other label in the tree is
clipped to 160 by the helper. Both names now go through
textutil.BoundLiteral at that length: controls, bidi overrides and
invisible characters show as visible escapes, and so does a quote, so a
name cannot close its own quotation.
The Windows helper sent keys to whatever held the application's focus and
answered success. With Notepad's error dialog up, `type "hello"` returned
Done:1 while the text went to the dialog's OK button (#11366).
The helper now reads two structural facts, never a title or a message:
- a Win32 modal: an enabled window of the process whose owner chain holds a
window of the same process it has disabled (MessageBox, DialogBox, common
dialogs);
- a dialog drawn inside its window that declares WindowPattern.IsModal, which
is what Windows 11 Notepad shows: one top-level window, still enabled, with
a XAML popup holding its input.
With them:
- type and paste are refused as computer.blocked, before anything is sent,
when focus sits in a modal on an element that takes no text: a letter there
can be the dialog's mnemonic. Typing into a field of a modal (a file name
in a save dialog) still goes in;
- a ref or point step on an element in a window the application has disabled
is refused as computer.blocked, naming the modal that holds it;
- key and hold_key still go (Enter and Escape answer a dialog) and say which
modal they went to;
- type, paste and set_value read the focused element's value before and
after: confirmed when what was typed is in, suspected_noop when nothing
changed, unverifiable when it cannot be read (no value, a password, or a
value too long to compare). Values stay in the helper; set_value no longer
sends its read-back across the protocol;
- a snapshot lists the modals, and marks the modal's line.
The fixture gains a message box mode and an owned form with a text field;
the live tests run in the logged-in desktop session.
A computer_act step that the helper accepted read to the model as done,
whether or not anything reached the target. On Windows, typing into Notepad
while an error dialog held its input returned Done:1 and the text went
nowhere, and the dialog showed in the tree only as an unnamed window.
The helper protocol gains three optional fields, all read from structure:
- a step's result may carry an effect: a closed class (confirmed,
unverifiable, suspected_noop), the evidence kinds it rests on
(value_readback, value_unchanged), and the modal its input went to;
- a failure may carry blocked_by, with the new code computer.blocked for
input a modal would swallow;
- a snapshot may list modals: ref, title, and the window each one holds.
The kernel owns the grading contract: a class without the evidence that
proves it, an unknown class, or no effect at all (an older helper, or the
macOS helper until it reports one) settles as unverifiable. A step that
sends nothing, such as wait, carries no effect. computer_act says each
step's effect after its note and names the modal in a blocked failure; a
snapshot opens with the modals holding the application's input.
Tool schemas and descriptions are unchanged, so the cache-stable prefix is
byte-identical. Approval is untouched: grading only adds information, and the
new refusal only sends less.
The replacement now keeps the size and restores the mtime, so only the digest
comparison can tell it apart; it fails with the digest taken after the probe.
Refs #11434
The digest recorded for a persisted proof is taken before the launch, so an
executable swapped during the probe is not vouched for. The Probe field comment
now says the memo is private while proofs still persist.
Refs #11434
A failure with no sentinel on the read side (missing object, bad content
reference, size or digest mismatch) was reported as a failed copy. Reads now
default to corrupt, writes are classified separately, a folder without a
manifest is not listed as a session, and each copy button names its session.
Bash discovery launched each Git Bash candidate serially (10 s each), then a
PowerShell check, and remembered the proof only in process, so every cold start
repeated it in front of the Studio handshake. Candidates are now probed with a
hedge, the search shares one budget and falls back with probe_timeout, and a
successful launch is persisted keyed by path, size, mtime and content digest
(treated as a miss on any doubt). Boot gains shell, prompt and environment
phase marks so the start-up trace names every stretch.
Refs #11434
The free-text box of a question card stayed display:none until Other or a
single pick was chosen, so a card with only fixed options showed nothing
typeable while the run line read "Waiting on you" and the open plan kept
turning its in-progress arc.
The box is now always present on a live card and typing into it is an
Other answer, so there is one control for the free-text path. The run line
names the wait ("Waiting for your answer") and the plan stops turning its
arc while a decision is pending.
The legacy import dropped unreadable sessions behind event.Discard and
returned only a warning count. The migration layer now produces one skip
record per session (name, source path, reason code taken from the reader's
sentinel errors), the import response carries them, and the storage panel
lists them with the fact that the originals are untouched.
Problem: The browser contrast guard timed out opening Settings after the
duplicate sidebar footer was omitted beside visible icon navigation.
Root cause: The icon rail's Settings and Account controls did not carry the
chrome.settings and chrome.account identities declared for those actions.
Fix: Declare each footer action identity alongside its icon, label and
destination, and render it on the rail button. Extend the interaction test to
verify those identities and destinations through both footer locations.
Verification: The new identity assertion failed on the previous rail and
passed after the fix. All 36 related interaction tests, production and perf
builds, ui-census invariants and 10 fixture reports passed. The CI browser
guards (intrinsic, contrast and wheel) plus perf/side.mjs passed. Formatting,
go vet, make lint, make check, builtin/boot Go tests and diff checks passed.
A rule such as ask=["rm"] parses but its tool name matches no tool, so it never
fires and the user believes a boundary exists that does not. Saving a rule whose
tool no registered tool (or MCP/extension namespace) can match is now refused
with permissions.rule_unknown_tool; rules already in the file are kept, listed
as dormant in the permissions panel, and announced once at load with a typed
permission_rules_dormant notice.
Store.roots canonicalised every directory up to three times per call, and
BuiltinSubagentTools ran a full discovery pass for each of the five built-in
skills it checks. On Windows each canonicalisation is a chain of directory
lookups, so every boot.Build paid for around 120 of them in this one step.
Refs #12521
resolveTurnImages and withVisionRouting loaded the full project config on
every turn to decide how to route images, including turns with no image at
all, where neither answer is used.
Refs #12521
internal/assembly/boot took 478-717s on the Windows runner across 12 push runs, so the 12m per-package timeout panicked. The full step now tees go test -json and, on failure, prints the failed tests' output and the 15 slowest boot tests.
tool.after can report a finished write as failed, and a move can stop
half-done, so a failure is not proof that nothing was written. Non-prose
targets of a failed named-path call now keep the generic check owed. The
declared-checks cause also covers checks captured at task start.
Refs #12519
The crash files are the runtime's raw output and are not redacted, only
shell.log and host.log are. The notice now follows the same language the
page draws in instead of the machine locale.
A Studio kernel that died of a runtime fatal left no readable trace: its
stderr sat in host.log, which request logging rotated away every few
minutes, and the shell quit without recording how the kernel ended.
Routes that are polled declare themselves with routine(); a successful
request to one logs at Debug, a refused one stays at INFO. The kernel
mirrors runtime crash output into a bounded crash directory the shell
names, the shell quotes the tail into shell.log and the next launch tells
the person once.
Failed non-named-path mutations leave the extent of the change unestablished,
so the prose-only waiver no longer applies. The ask now names an unestablished
extent and declared checks as causes, spells paths from the workspace root, and
the trace fixtures are regenerated.
Refs #12519
The waiver read the whole workspace scan, so a README edit in a repository
with any code was still held to a verification command. It now reads the paths
the task changed: all prose (.md/.rst), scope established, no declared check.
The ask also names the changed paths that keep it owed.
Refs #12519
Git before 2.31 echoes the unknown option as an extra output line, which
Open reported as not-a-repository. Normalise the git dir, common dir and
work tree in Go instead, and do the same for the index path lookup.
The refusal echoed the command verbatim, so a long or multi-line command
could flood the message or forge a Cause line. The subject now goes through
textutil.BoundLiteral. A compound statement whose subcommand is dynamic
(git $X; ls) is attributed to shell expansion instead of leaking the
internal placeholder as an unknown program. The declared-writer cause no
longer claims a tool declared that it changes state.
Refs #12508
The install preview always shows every high-risk action, and an entry
with "tier": "eager" is high risk. A marketplace is capped at 64 plugins
and a skill repository at 200 skills, but a .mcp.json had no cap: a
package could ship tens of thousands of eager entries, bounded only by
the 2 MB remote read, and every one of them would be displayed.
parseMCPJSON, which planning runs for both a local and a fetched
.mcp.json, now refuses a file declaring more than maxMCPJSONServers (64,
the marketplace cap) with ErrInvalidManifest, before any entry is turned
into an action. The refusal is a hostFactError, so the caller reads the
declared count and the limit rather than an omitted detail.
With kind=mcp, a fetched URL whose path names a .mcp.json now returns
its parse error, as a local .mcp.json already did, instead of
ErrUnsupportedKind; the over-limit refusal otherwise never reached a
remote caller. Any other document fetched with kind=mcp, such as a
SKILL.md, still answers ErrUnsupportedKind.
Refs #12488
The planning gate recorded every call it could not prove read-only as a
side effect and told the model it "would change state", with no command,
no cause and no alternative. The effect is now Unclassified when the host
only failed to prove a read, SideEffect when a tool declares itself a
writer or an argument makes a reader write, and the refusal carries the
classifier's structural proof (unknown program, shell construct from the
parser, writing arguments, background, undeclared tool). The marker now
defines read-only shell in the same terms.
Refs #12508
1.x suspended to the shell on Ctrl+Z, as terminal programs do; 2.x left
the key unbound, so it did nothing and there was no way to drop to the
shell without quitting.
Ctrl+Z now returns tea.Suspend from shortcutKey. Bubble Tea releases the
terminal (alternate screen, mouse tracking, bracketed paste) before
stopping the process and restores and repaints it on fg, so nothing of
1.x's manual mouse reset is needed. On Windows Bubble Tea has no job
control and ignores the request. GUIDE.md and its Chinese version list
the key.
Refs #12144
After scrolling back through the transcript, Enter on an empty composer
did nothing; getting back to the live output took Ctrl+End or the wheel.
1.x jumped to the bottom and followed the tail again on an empty Enter,
idle or while a turn ran.
An empty (or blank) Enter now does the same in full-screen mode, through
followTail, which Ctrl+End also uses. It still sends nothing. Inline mode
has no in-app view to move and is unchanged. GUIDE.md and its Chinese
version note the behaviour on the Enter row.
Refs #12144
Swapping in this build's system prompt after a model switch made the
transcript differ from the 1.x log, so the save that follows moved the
conversation to a new session. The system prompt is host contract, not
conversation; the unchanged check now leaves it out.
Refs #12474
The list scrolls inside a height the composer's aux area caps, so an editor
grown to its text could be taller than the list and get cut off at an edge.
The fit hook now takes the room left by the list, and the edited row is
scrolled into view. The browser check asserts the whole row sits inside the
list's visible area at desktop and phone width.
Refs #12477
The effort field is now resolved from the entry's reasoning protocol rather
than its kind, so an auto entry that resolves to a bespoke shape (Zhipu,
MiniMax, LongCat, DeepSeek) no longer claims reasoning_effort. The catalog
carries the protocols the field holds under and the panels read it instead of
keeping a copy. Adds the Anthropic none case and an end-to-end check that
existing effort-only configs keep their request bytes.
The package doc still said a cut of 50 actions sets previewTruncated, and the
field comment said escaped text sets it. The test-only PreparePlugin option
is marked as such.
#12389 and #12035 each added the tsLookFile constant and an appearanceView mirror; the two merged cleanly as text and left tools/repolint unable to compile (tsLookFile redeclared). Keep one constant and one appearanceView entry, plus the zoomRangeView entry #12035 added.
PreviewTruncated also fired when only whole steps were left out, so the
confirmation said text had been shortened when none had. It now means that
text was cut or escaped, and HiddenActions carries the step count. The card
no longer repeats the plan-level sentence; it shows a short marker aligned
with the other rows.
The protocol table now declares where a chosen effort level lands in the
request body (reasoning_effort, reasoning.effort, output_config.effort). The
protocol catalog and the provider list carry it, and the add, detail and edit
panels word their help and example body from it instead of naming the Chat
field for every wire.
The Responses wire now takes the same thinking switch, reasoning_protocol and
declared levels as Chat in all three panels and in the edit endpoints.
reasoning_protocol=none is honoured when resolving the effective effort, and
a declared openai protocol lists its own ladder as the request vocabulary.
The shrunken-viewport case is renamed to say it checks the box stays within
60% of a shorter viewport with the caret at the end. The queue editor check
now compares against its computed cap and the room the list leaves. The
editor's own max-height was dead (the composer's textarea rule wins), so it
is removed again.
prefer=powershell tries Windows PowerShell 5.1 before pwsh, so advising it to a pwsh 7 user would trade away && chains. Advise prefer=pwsh when the interpreter in use chains.
Auto-detection never looked at where Git for Windows records its own install, so a Git outside the standard roots whose git is not on PATH was invisible and the session silently ran PowerShell. Add that registry location as the last bash candidate source, make a failed probe say whether it timed out, give the probe 10s instead of 3s for a cold start, and write the fallback warning that the startup notice already carries.
The theme category check had no test that reached ErrNotTheme. A marketplace
of fifty theme packages plus one with a hook must be refused and keep the
hook step visible in the envelope; sixty pure themes must pass. A table
sets each count field and the runtime alone, so dropping any term from the
sum is caught. install_source gains a test-only PreparePlugin option so the
market layer can plan a marketplace without a network.
The theme gate read the bounded preview, which lists at most 50 steps while
a marketplace may hold 64 plugins and a skill repository 200 skills, so a
hook or runtime past the cut skipped the check and the confirmation. The gate
now reads the unprojected actions; the preview keeps every high-risk step,
caps the rest, and reports how many it left out; the real skill count is
shown. Package inventory returns the package's own text again, and the
display projection is a separate call.
The FULL TRUST reason embedded the runtime command inside a sentence that was
then stripped as prose, so an unterminated escape sequence in an argument
swallowed the warning. The command is now rendered literally after the host
sentence. BoundProse reports removal as well as truncation, BoundLiteral
cannot be mimicked by typed escapes, and colliding display keys are flagged.
Stack on the refreshed read-only branch chip. Guard checkout atomically against all active panes and unfinished jobs in this host, preserve queued input cancellation, honor existing writer leases, and protect ignored local files including late-created collisions.
Problem: Usage, feedback, account/workspace and settings appeared twice when
the global icon navigation and workspace sidebar were both visible.
Root cause: Sidebar rendered its footer unconditionally, independent of the
navigation visibility already owned by App.
Fix: Pass the existing navigation visibility into Sidebar and render the
footer only when navigation is hidden. Preserve the fallback entries in off,
collapsed-only and narrow-window modes, plus existing feedback and settings
routing. Update layout/parity docs, pending release notes and fixture images.
Verification: 36 relevant frontend interaction tests, 11 release-note/link
tests and the frontend production build passed. Formatting, go vet, make lint,
make check, builtin/boot Go tests and diff whitespace checks passed. Browser
fixtures cover light/dark wide windows and narrow drawers.
The queued-line editor took its height from the number of line breaks, the
same defect as the rewrite box, so it now uses the shared fit-to-content hook.
Both height caps use dvh (with a vh fallback on the same line) so a soft
keyboard shrinking the viewport shrinks the cap. The browser check gains the
queue editor and a shrunken-viewport case; the harness can read a seeded
queue line back.
A failed job's notice now carries the same JobNotice payload as finished and
killed ones, plus the job function's own error text, so a frontend words
it from the label or id and shows the raw error beneath. A failure stored
without a payload still renders its text. Adds an emission test for the
killed notice.
Rebase the read-only branch chip onto the current Pane split. Keep the stylesheet within budget, register the complete workspace Git wire contract, and verify refresh after turn completion.
The rewrite box took its height from the number of line breaks, so a long
paragraph written as one line showed about 60px of it. The composer's
fit-to-content logic is now a shared hook used by both, with the box capped
at 60vh. A browser check covers a long single line at desktop and phone
width in both themes.
The completion notice carried only English text with a numeric id, so a
frontend could neither localize it nor name the job. Finished and killed
notices now carry a code and a JobNotice payload (kind, id, label); a
failure carries a code and keeps its diagnostic. Studio and the terminal
UI word them from the code; uncoded notices from older sessions still
render their stored text.
A text-less compaction_progress frame now tells a frontend the streamed
digest was discarded, so a retried summary does not append to the cut one.
The pressure effect test counts attempts as pairs with a larger retry cap
instead of capping raw request count. Adds a shared-window room test and
updates SPEC.md.
The summary request is capped at 16K output tokens and reasoning shares that
cap, so a model with no low reasoning tier can spend all of it and have the
digest discarded. Truncation is already a typed finish reason, so the host
retries once with the cap doubled (bounded by the model's configured output
ceiling) and fails with the cap and retry recorded when that is cut too.
The rail clipped itself to its animated width, which cut off the hints that
hang outside it, and shared a z-index with the workspace rail that paints
later. The rail now keeps its open width and slides off screen instead, and
sits one layer above the workspace rail.
Refs #12462
Only the walk skips VCS store entries. An explicit write into a VCS store is a
change like any other. The snapshot-store exclusion is a path convention for
de-noising, so a symlink anywhere on the way disables it.
The unverified-change gap carried a sentence the kernel wrote in English, so
every language showed it; it now carries only the declined command. Changes the
host or a tool owns -- the task-snapshot store, VCS stores (including a .git
file), and directories tagged with CACHEDIR.TAG -- no longer count as work.
Refs #12457
The tests that fake Windows name probing on every OS spelled PATHEXT in
upper case while their fixture files are lower case. Windows file lookups
ignore case, so the real lookup finds mcp.cmd for .CMD; a Linux filesystem
does not, so under the fake the probed name missed the fixture and the
tests failed there. The seam fakes the probing, not the filesystem: spell
the extensions as the fixtures are.
Adds the off-Windows counterpart: where nothing probes extensions, a .cmd
beside the command does not affect the binding, and the command file does.
A fixed entry is upgraded from next to a concrete version only when a merged
closing pull request is contained in a studio tag whose release page is neither
a draft nor a prerelease. Prerelease status is read from the releases API, not
from a version cutoff.
A pack with a sky drew a blurred cloud bank under six backdrop-filter panels,
so every frame re-blurred 41% of the window and the idle window held a GPU
engine near full on integrated graphics. Frosted panels that are already
72-94% opaque blurred nothing visible, and the grain layer forced a
full-window overlay blend on every frame.
- Sky: CSS breathing animations removed; the canvas redraws on a 15 fps
timer and only while the window is shown and focused (presence.ts).
- backdrop-filter kept on the composer and the transcript backing only;
the other panels get a higher flat tint instead.
- Grain drops mix-blend-mode.
- New Appearance setting, Visual effects: Full / Reduced (rx-effects,
immediate, per machine). Reduced removes remaining blur, text glow, the
grain and the sky.
- perf/effects.mjs guards all of it in a real browser.
Clicking .html/.htm in the workbench explorer shows the file as a page in the
shared browser through the existing disk-path open, reloading when the file
changes on disk. Windows that draw no browser views and remote workspaces keep
the source editor.
The model menu now always opens with a search field, groups models under a
sticky provider heading with a letter chip, uses compact one-line rows and
grows with the window (up to 640px) instead of a short fixed box. Rows keep
the wire format and show a reads-images badge where the model declares it.
Enter no longer lands on the manage action, and an empty search says so.
An enable recorded for a server that .mcp.json or a project reasonix.toml
declares held for the server's name. The row now records a digest of the
declaration as written (type, command, args, env, url, headers), the project
.env values it expands, and the content of the workspace files it names:
its arguments, and every file the launcher could start for its command,
found by the launcher's own lookup (base/proc): the declaration's PATH for a
bare name (every Windows spelling of it, in key order), and PATHEXT
siblings on Windows, dotted names included. A different declaration reads
as ActivationChanged: off and awaiting the user. Rows written before this
carry no digest and ask once.
- Specs for a repository-declared server carry a LaunchCheck run before
every start and redial; env overrides merge in key order.
- Why a server waits is a typed code (awaiting_user_decision,
changed_since_enabled) on MCPHealth, the serve and ACP wires and the TUI;
Studio and the TUI word it in the reader's language; Studio shows the
launch line beside the switch.
- A connect or reconnect by name refuses a repository server in any pending
state with ErrMCPApprovalOwed (serve: mcp.approval_owed) and its launch
line; a server from the user's own files is still answered by a connect.
- Build reports each held project server once, so `reasonix run` prints
one warning line per server; serve prints the same at startup; /mcp and
`reasonix mcp list` show them. Repository-chosen names are sanitised
wherever they are printed, and run diagnostics sanitise every line.
- `reasonix mcp enable` prints the approved launch line; a project install
records the decision for the declaration as written; doctor --live
starts only what a session would.
Every test for the project MCP approval change, ahead of the change:
- boot effect tests through the real assembly and the settings switch: a
rewritten declaration, a launcher changed mid-session and an executable
swapped behind the declaration's own PATH must not start on an earlier
enable; held servers are reported once each at build; a repository's
server name reaches no notice as control sequences.
- config: the enable covers the declaration, the .env values it expands,
the files it names, the executable the launcher resolves (declared PATH,
every spelling of PATH, PATHEXT siblings including dotted names), legacy
rows ask again; base/proc holds the launcher's lookup.
- plugin: the launch check runs before start and redial; env merges in a
fixed order.
- control/cli/tui/frontend: a connect by name never approves a repository
server that waits; why it waits is a typed code on every surface; /mcp,
`reasonix mcp list`, serve's startup warning, run diagnostics and the TUI
panel print a hostile name as plain text; doctor --live starts only what
a session would.
The form moved focus from the tab into the body in a frame callback queued from the environment promise. The fieldset is disabled until that environment is committed, so a frame that fired before the commit called focus() on a disabled textarea and focus stayed on the tab. The move now runs in an effect on the environment, after the commit.
The unread interaction tests emitted wire events through a listener map that was only filled once the pane subscribed, and a missing listener was a silent no-op. The first test of the turn-ending group waited for the port to exist, which is earlier than the subscription, so an event could be dropped under load. emit now waits for the listener.
docs/GUIDE.md lists Ctrl+Enter (or /steer) as the key that adds guidance
to the active turn, and 1.x bound it so. 2.x bound only Ctrl+S, so on a
terminal that reports Ctrl+Enter the documented key did nothing.
While a turn runs, Ctrl+Enter now sends the composer as steer through the
same path as Ctrl+S. Idle it does nothing, as in 1.x, so a press meant as
a newline never submits the draft. Ctrl+S is unchanged.
Refs #12144
With in-app mouse capture on, a right-click copied an active transcript
selection and otherwise did nothing. 1.x pasted the clipboard's text on
a right-click with no selection, and docs/GUIDE.md and docs/CLI.md say
2.x does too, so the documented paste never happened.
A right-click with no selection now pastes through the same path as
Shift+Insert: text only, through the normal paste handling, with the
existing hint over SSH. It pastes only while the composer is drawn; the
condition that draws it moves into composerShown so the view and the
mouse read one answer. A right-click on a selection still copies it.
Refs #12144
Up and Down recalled history only while the composer held a single line.
Once a recalled entry had several lines, Up moved the caret to its first
line and then stopped: nothing older could be reached, and a multi-line
draft could not step into the history at all.
Up now recalls from the composer's first line and Down from its last, as
1.x did; between them the keys move the caret as before. A single line,
soft-wrapped or not, behaves exactly as it did.
Refs #12144
Cause: 30 days of Studio UI changes landed without screenshots or review, and a visible control was hidden without a linked issue or a release note.
The PR template had no UI requirement and no table tracked 1.x features.
Refs #12427. The rail moves left of the sidebar, gains the feedback entry, and the Appearance setting becomes always / only when the sidebar is collapsed / never. Stored on and off values keep their meaning.
The /mcp panel opened a server's tools with Enter and backed out with
Esc only. 1.x's MCP manager also went in with l / Right, backed out one
level with h / Left, and closed from anywhere with q, and docs/GUIDE.md
(both languages) still lists those keys, so pressing them did nothing.
The panel now takes l and Right as Enter, h and Left as Esc (back to the
list, or closed from the list), and q closes it from the list or a
server's tools. The repository-declared confirm line is unchanged: any
key but y still cancels it.
Refs #12144
The ask card took only the arrows: Up/Down (and Tab) to move between
rows, Left/Right to move between questions. 1.x's question card also
took j/k and h/l, and docs/GUIDE.md (both languages) lists them for it,
so pressing them on 2.x did nothing.
The card now takes k as Up, j as Down, h as Left and l as Right, with the
existing wrap, clamp and auto-submit stop. A typed answer or note is
unaffected: the composer gets those keys as text before the card does.
Refs #12144
The approval card moved its cursor with the arrows and Tab only. 1.x also
took j/k and Ctrl+N/Ctrl+P there, and docs/CLI.md says approval rows use
the same row-selection keys as the pickers, so pressing them did nothing.
The card now takes k and Ctrl+P as Up and j and Ctrl+N as Down. None of
them is a row shortcut (y/a/p/n/x), so no answer changes meaning.
Refs #12144
A call reached through use_capability only learned its delegation profile after the target finished, so the deck's sub-agent list and the running count missed every sub-agent until it ended. The profile is now announced when execution starts.
A notes response is accepted only when its declared type is markdown or plain
text, so an error page served with 200 is refused and never written. The cache
name is lower-cased with a digest for mixed-case prereleases and over-long
versions are refused. Expanding a row on a narrow window keeps its label and
date. Error wrapping and the wait group follow the linter.
The kernel serves GET /studio/versions/{version}/notes from the release mirror,
keeps each fetched document on disk for good, shares one request between
concurrent readers and refuses by typed code. A row whose catalog entry names
notes gets an inline view with images off.
The notes are optional, so a failed upload or head-object warns and writes the
catalog entry without a pointer instead of failing the Mirror to R2 step.
Studio release workflow uploads studio/notes/<version>.md ahead of the catalog
write and gives the new entry an optional notes field. A one-time script
backfills the entries already in the catalog.
pendingAsk returned an arbitrary entry of the ask map, including one still queued behind the prompt lock. When the second ask took the lock first, the test answered the queued one and then waited for it while the other held the lock for the user: a hang until the package timeout.
useWallet treated the empty account of a pane whose status had not arrived as a source, so the first status turned it into a source change: the wallet read that had already landed was emptied and read again.
Update capped the lock wait at 5s even for a caller that supplied its own deadline, so the number of writers a queue could hold was fixed by the package rather than by the caller. The serve tests now pass a budgeted context and fail on the returned error instead of reading a swallowed one as a wrong count.
The host appends a background-job-finished notice as a user-role message once the job ends. When it landed before the CHECK turn composed, the provider read the notice as the last user message, took its default branch and never issued wait/read, so the answers it recorded for w1 and r1 were empty.
Every config load re-parses each enabled plugin package, and a settings sheet
fires a dozen such loads at once, each queued behind the package store's lock.
With a 293-skill package a burst costs one full parse per request. Callers now
wait for a parse that starts after they arrived and share it, so a burst costs
at most two.
Every type the desktop re-declares by hand is held to the kernel's, and
src/port/look.ts was the one file under port/ that no pair named. The theme
a person is looking at is all of it: the pack listing, the pack's two shape
blocks, the import receipt, and the reader's size and wallpaper.
Declared and not inferred: no spelling tells a mirrored contract from a
struct that merely carries json tags.
The desktop-sessions-v5 by-id store carries no workspace, so every session
from it landed in the workspace picked in the dialog. The owner is recorded
in desktop/workspace-state-v1.json; read it from the picked folder and route
each session to its workspace while that folder still exists.
Pane.tsx sat at the 800-line ceiling with twelve open pull requests editing
it. The view (flow, analysis, browser) lived in component state, out of reach
of the tab strip and of any setting that will place tabs.
The view now lives in state/paneview.ts, keyed by pane id and owned by the
mounted instance, so a remount for another conversation still starts on the
conversation. Submit, surface slots, the meter row, the plan fold and the run
line move to files of their own; props and the report type move to
panetypes.ts. No visual or behavioural change.
Adds css-important, css-size and stray-file rules to repolint, deletes the
stray Settings.tsx-e, runs the intrinsic, contrast and concurrent browser
guards in the studio workflow, and adds a report-only wheel-scroll probe.
The rail was hidden by the studio layer's prototype alignment, which left Nav.tsx rendered but display:none. It is drawn again, beside the workspace column, when this machine has it on (default) and the window is wider than the scene fold.
The BrowserWindow carried no backgroundColor, so any pixel the page does not
cover (frameless window at a fractional display scale) showed the default
white. The ground now follows the stored rx-theme, or the system for auto, at
creation and when preferences are saved or the system theme changes.
The sidebar folds a conversation's recovery copies under one row, but
/tree/sessions/remove deleted only that row's file. The next copy then took
its place, so the delete looked like a no-op with the order shuffled.
Remove the lineage the way archive already addresses it, leaving siblings a
pane has open.
A 400/422 on a request whose history carried reasoning the connection
could not replay was labelled "the endpoint wants the thinking content
sent back", whatever the endpoint actually said. Dropped reasoning is a
fact about the request, not a cause the host observed, and the endpoint's
refusal carries no typed signal separating the two. The hint is removed;
the refusal now shows the status guidance and the endpoint's own words.
Search and list dropped results silently: hits below the relative score
floor (KeepTopRelativeScore compacts in place, taking the count with it)
and everything past limit. A reader could not tell a complete result from
a truncated one, while the empty-result message already warns that no
result is not proof of absence.
Both paths now carry the note automatic recall already uses for its own
omissions, counting both reasons and staying silent when nothing was
dropped. The spelling lives in one helper the recall block calls too, so
the two cannot drift apart; its output is unchanged. The count is one
function as well, so the case where both reasons fire is tested directly
instead of only through fixtures. Empty results keep their guidance: the
relative floor always keeps the top hit, so empty still means nothing
matched.
Refs #11853.
File tools resolved a path through the filesystem before comparing it with the
workspace, so a path spelled as a network share was looked up first. On Windows
that lookup is itself a connection to the machine the path names.
A network path is now answered by its spelling alone, with no filesystem call:
it passes only below a workspace root that is itself a network path, compared
lexically and case-insensitively, and is otherwise refused with
workspace.network_path_outside_scope. The check is one function in fileutil and
sits at the entry of the read tools, the writers, their previews and write-path
resolution, write claims and path observation. Local paths and network
workspaces behave as before.
A path spelled as a network or device path is now answered by its spelling
alone, before any filesystem call, and is refused with its own code
(browser.network_path) instead of the generic outside-workspace one. The
check lives in fileutil next to the containment helpers and covers page
loads and file uploads.
Both boxes submitted on Enter regardless of pointer type; they now ask
touchKeyboard() like the composer does. The queue editor also ignores Enter
while an IME composition is open, as the rewrite box already did.
The anonymous ping was a single attempt at launch with a 1 s total budget; a
failed attempt dropped the day's claim and nothing tried again until the next
launch, and a host left open past UTC midnight was never counted again. The
reporter now retries a failed ping with bounded backoff, waits for the next UTC
midnight (plus a splay) after a success, and stops with its context. Consent is
re-read before every attempt, so turning the setting off stops the next send
without a restart. A refusal the server marks permanent (400/401/403/404/410)
is not retried for the rest of the UTC day; 429, 5xx and network errors back
off. Request timeouts are relaxed since the work is off the launch path. The
payload is unchanged.
The interactive session that carried the first-run consent and the reporter was
removed, and the replacement terminal UI never started either, so CLI 2.x users
were neither asked nor counted. runTUI now runs the documented consent on the
plain terminal before the UI takes the screen, starts the reporter only after a
mode is stored, and wraps the controller sink so the existing bounded counters
flow.
An ask or tool-approval wait kept the session's claim for as long as the user
took to answer, so other sessions were blocked on paths nobody was writing.
The claim is now yielded for the wait and taken again, queueing behind any
session that claimed the extent meanwhile, when the user has answered.
Refs #11531
The address bar read "D:/DevCode/x.html" as host "d" over http. A drive
letter plus a separator, a leading backslash pair or a single leading slash
now names a file: URL, parsed once in Go (browser.LocalPathURL, used by the
agent's checkURL) and once in the window (localpath.js), both held to one
shared table. The window opens local files for the person; a network share
is refused with its own code instead of a DNS error, and a page's own links
still never reach a file.
The summary request is one user message that ends on the last tool output, so
the model sometimes carries on the agent's work (pseudo tool calls, "next I
will ...") and the host installed that reply as the digest: a read-only fold has
no changed file for the coverage guard to compare.
- The user turn now ends with an explicit ask for the briefing.
- A reply with no level-2 heading, the shape the summary prompt requires, fails
with the typed code summary_not_digest. It is billed to the compaction, is
retried like any transient summary failure once the input has grown, and
degrades to the mechanical digest when the fold must free space.
- The code has frontend text in the TUI catalogues and the desktop table.
- Test doubles that answer summary requests now speak the heading contract.
The worker now states a profile on GET /v1/feedback/mine and names the
window in every feedback refusal. The kernel client dropped both: the
profile never reached a frontend and a 429 collapsed to "wait N seconds".
- feedback.Mine carries a validated Profile (null when absent or
incoherent; the last confirmed one while offline, cleared with the
install identity).
- Refusals decode error.params into a typed LimitError (window, reset
time, wait) for rate_limited, reply_limit and busy; serve forwards the
same fields through refuse.
- Studio shows the level row, progress track, limits, trust note and the
badge artwork (single-colour SVG on text tokens) in My feedback, and
says which window a refusal ran out of and when it resets.
- The terminal's /feedback list prints the same standing and refusals,
worded through the i18n catalogue.
Refs #12025
keepToolCallGroup kept every result of a parallel tool-call group when any one
failed. Models batch 4-11 calls per turn, so one failing test pinned the whole
batch of reads, the candidate exceeded the 50% checkpoint ceiling, the billed
summary was discarded, and the non-transient block was never retried.
- The retained assistant turn now issues only the calls whose results stay; the
folded copy carries the rest, so the summarizer and the coverage guard still
see them and no request issues a call nothing answers. Turns whose call ids
cannot be told apart stay whole.
- When the retained failures alone would exceed the ceiling, the fold gives up
retaining them before the summary is requested; the coverage guard still makes
the digest name each failed command.
- A candidate_above_ceiling block is released by input growth below the physical
ceiling, and an overflow on a changed input may fold, instead of holding
forever.
The Settings subagent row writes subagent_model, while an entry the CLI
writes under subagent_models is read first for its profile, so the row
looked inert. The kernel now lists those entries per role with the file
that holds them, the row names each one, and a user-config entry can be
cleared from the row.
Refs #12151
A brokered pane takes its default from the home machine's catalogue, so a
default_model written next to the pane's own kernel was never read and the
row snapped back. The default is now its own request, POST /default-model,
sent to this machine's pane; the pane's switch stays a plain /model. A
brokered pane refuses the request by code instead of ignoring it.
Refs #12043
agent.subagent_effort was validated only against the parent model and then
applied to whichever model a subagent selected (subagent_models.task, a
skill's own model, a per-profile model, a per-call model) through
NormalizeEffort, which remaps across providers or lets an unsupported
level through. The default is now resolved per executing model by one
function; a level that model does not declare is dropped with a warning and
the model keeps its own default. Explicit efforts still go through
NormalizeEffort, whose refusal now satisfies errors.Is(err,
config.ErrEffortUnsupported) and names the model and level.
The ACP-specific subagent resolver and task defaults had no caller (ACP
sessions are built by boot.Build), so they are removed.
Refs #11871, Refs #11872
The notice appended to bash output when a command mentions the session store
read like a refusal: it said to stop retrying and report, and when the command
matched nothing it was the only text returned. A model that had just been told
to read a spilled output gave the output up. State that the command was not
blocked and name read_file/grep for spilled outputs.
Cancel had no bounded wait, so a stop the kernel never acknowledged left the
button on "stopping" forever, and stopping only cleared when the running flag
dropped. Cancel now shares the bounded acknowledged post with decisions, which
fails with a typed DeliveryError (kernel_busy, unreachable, ui_stalled). A
deadline that fires far past its time on the monotonic clock is reported as
this window being stalled instead of a busy kernel. A stop also ends when the
kernel's status reports no live turn.
Refs #12232
The shell held nothing against system suspension, so a laptop that went to
sleep overnight froze every running session. The kernel now answers how many
panes have a turn in flight (GET /tray/running, local and remote, read from each
controller), and the Electron shell holds a prevent-app-suspension block for as
long as that is non-zero, reconciling against the answer on every poll so a
closed pane or a dead kernel cannot leave it held. A machine-level switch in
Settings > Appearance > Window turns it off.
Build windows/arm64 on a native windows-11-arm runner and carry it through the
signing chain as a second leg of every Windows stage: one tree per job, legs
serial under the certum-signing group, a failed leg reruns alone. The package
check learns app-arm64.7z and refuses any PE image that is not the leg's
architecture, a new install smoke runs each installer on its own architecture
before publish, and publish counts, names and hash-checks both architectures.
The arm64 installer is packed with the 7-Zip BCJ filter: the default ARM64
filter yields an archive the NSIS extractor cannot decode, and the installer
then exits 0 with every executable missing. Signing contract tests pin the
matrix, the serial legs and the secret-free stages.
Add a windows-11-arm leg to the Studio package job: the kernel and the
computer-use helper build for the runner's architecture, every PE image in the
bundle must carry the package's machine type (the installer-written
elevate.exe and uninstaller are 32-bit by construction), and the package is
started, installed and uninstalled where it is meant to run. An arm64
installer now refuses a machine that is not ARM64 Windows instead of
installing nothing, and a second job checks that refusal on an x64 runner.
The arm64 installer is built with the 7-Zip BCJ filter: 7-Zip applies an
ARM64 filter to ARM64 executables by default, the NSIS extractor cannot decode
it, and the installer then exits 0 with every executable missing.
The ARM64 leg runs on pull requests only when files it verifies change.
Build and verification only: no secrets, nothing is uploaded to a release.
With no browser tab, following a link asked the kernel for the link's tab
and the column, mounting at the same moment, asked for an about:blank of its
own. Which of the two the selection rules landed on depended on reply and
poll order, so the blank page was sometimes the one shown.
openPage remembers what is being opened until the kernel answers, and the
empty column takes that page instead of requesting another.
Refs #12251
The sub-agent chip summed every delegate in the session, finished ones
included, while the background-jobs chip counted only running jobs (and
fell back to the total when none ran). Both chips now show what is
running; the sub-agent popover reports "N running / M total" and keeps
the finished ones listed.
An optional extension whose sidecar is not running was reported as an
English sentence with no code, which the composer's notice bar clipped
at its edge and which named no next step.
dispatch now exposes ErrNoLiveSidecar and a SidecarDown hook; boot turns
it into a notice coded extension_skipped whose Detail is a typed payload
(extension, point, reason). The desktop words it from the payload with a
button to Tools and integrations and lets the bar wrap, the TUI words it
from the i18n catalog, and ACP chunks carry the code and payload.
Refs #12258
The form card's submit awaits onAnswer and clears its in-flight flag in a
finally block. The tests resolved that answer on a 20ms timer they never
waited for, so the setState could run after the jsdom environment was torn
down and vitest exited non-zero on 'window is not defined'. The tests now
wait for the answer to settle, and an afterEach fails any test that leaves
one in flight.
An entry parked as uncertain with code steer_unapplied (guidance the turn
never read, e.g. after a stop) kept the queue paused, and the panel offered
Retry only for blocked entries, so Resume was a no-op and the only way out
was withdrawing and resubmitting. Other uncertain codes may already have
run and stay take-back-only.
Rename the model-section action to "Read available models from provider" and
offer the same action beside the add-model field, with the failure shown there.
Light --warn is 4.58:1 on white and 3.86:1 on the floating layer, under the
4.5 text line. Text rules that set color to a bare --warn, --ok or --err now
use the matching ink token; borders, washes and glyph marks keep the base.
A structural test fails a new bare hue used as text.
useWallet now takes the account (connection) the wallet belongs to: a change
of account empties the wallet at once and reads again, and only the latest
read may land, so a quick double switch cannot leave the first account's
number up. Switching models within one account keeps the wallet and does not
re-read. An account with no wallet (204) or an unreadable one shows nothing
instead of the previous figure. A reading the source reports as unavailable
is drawn in the warn-ink token and carries a text label; the figure itself is
untouched.
Refs #12266
The updater and studio-manifest already key packages by GOOS-GOARCH; nothing
pinned what happens around a second Windows architecture. Cover both
installers resolving to their own keys, a manifest without the client's key
resolving nothing, a delta for another architecture meaning no delta here,
and document the platform-key rules in the release runbook.
TestEffectVersionControlRidesTheTurnNotThePrefix builds two controllers and
requires byte-identical system messages, but the environment section in that
prefix comes from the process-wide probe cache, keyed on PATH and shared with
every earlier test. The boot package runs longer than the cache's five-minute
TTL on Windows CI, so an answer an earlier test left can expire between the
two arms: the first arm reads it, the second re-probes, and a tool that timed
out once (a cold docker --version) appears in only one prefix.
The test now appends an empty directory no other test uses to PATH, which
keys a fresh probe answer for the first arm that the second reuses seconds
later. PATH lookup order and what each probe resolves to are unchanged.
A second TUI attached to a session kept an ask or approval open after the
turn ended without a decision receipt (the other screen skipped the ask or
cancelled the turn), blocking its input on a prompt the kernel had already
dropped. The desktop already seals these in sealTurn; the TUI transcript now
does the same on turn_done.
Xiaomi lists mimo-v2.6-pro, mimo-v2.6-flash and mimo-v2.6-pro-ultraspeed, and takes mimo-v2.5-pro and mimo-v2.5 offline at 2026-10-21 10:00 Beijing time with no replacement model.
A PreToolUse/UserPromptSubmit hook that could not be evaluated used to be
treated as absent: a malformed matcher never fired, a spawn error was
DecisionError (warn-level), and a payload json.Marshal error was dropped so
the hook ran against empty stdin and passed.
On gating events (and Claude-imported PermissionRequest) each of the three
now blocks, with a typed cause (ErrInvalidMatcher, ErrSpawnFailed,
ErrPayloadUnserializable) carried on Outcome and Notice. The model gets only
structured facts (code, step, event, scope, source, label), never the
script's output or the underlying error prose. Observing events still do not
block; they report the outcome to the user. Save rejects an invalid matcher
on tool-matcher events.
The tests that pinned the old behaviour (malformed regex "should not fire",
spawn-error => DecisionError on PreToolUse) are rewritten to the new contract.
101f2ca12 stopped composing the fact index into the cached prefix, but
SESSION_MEMORY_RETRIEVAL.md and REASONIX.md still said it joins the prefix
at session start. Only pinned bodies and standing instructions do; the
index is reached through the memory tool.
A cache hit never wrote the snapshot of the directory it was served to, so
when the 5 minute in-memory entry expired the next build probed live and
could render a different section, moving the cached prompt prefix. Adopt the
served answer into that directory's snapshot.
Also print the first differing bytes when the scheduled-run prefix test
fails, so a future flake is diagnosable.
The host appended a <context-budget> block to the turn tail at 75% and 92% of
the compaction trigger and raised a matching warning card. The model can still
ask: the read-only context_budget tool and its figures are unchanged, and its
description now says when to check, since nothing reports it unprompted.
The window opened a link in a new tab and then switched the active tab back
to the agent's. The page list was re-read between the two, so the panel saw
the agent's active tab move to the new tab and back, and its follow-the-agent
rule selected the agent's tab again: the link's page appeared but was not the
one shown. Session.VisitBeside opens the tab without activating it unless the
session has no active tab, so there is no transient to misread.
A prompts/get failure surfaced as the bare transport error, and the model
never learned the user had invoked a prompt. MCPPrompt now returns a typed
MCPPromptError (prompt, server, stage; matches ErrMCPPromptFetch) and owes
the model a turn-tail block describing the failed invocation.
Draw the kernel's typed `unread` on session rows (dot, bold title, spoken
name), as a count pill on folded project rows and as a dot on pane tabs.
A session is marked viewed through the pane's own endpoint when it
becomes the pane in front, when an unread row or pane is clicked, and
when a turn ends in the pane in front while the window has focus. The
mark is cleared optimistically and restored if the call is refused.
Refs #12230
/attachments refused with plain text, so the composer could only show
"Could not add" and left the reason in a tooltip. Refusals now carry
attachment.unsupported_image, too_large, empty, unreadable and write_failed
with params, the port keeps the code, the catalogues say each in both
languages, and the failed chip shows the reason with Retry.
Panes that are not on screen kept re-rendering on every streamed delta, and
each of those renders also rebuilt the reply actions handed to every settled
row. With nine sessions streaming the renderer's main thread saturated and
queued work (status reads, stop, settings) starved.
A hidden pane now takes the first token of each stream at once and coalesces
the rest into one batch per second; any other event flushes first and bringing
the pane forward flushes immediately. Reveal pacing, the thinking clock and the
wait clock only run for a pane that is shown. The reply actions keep their
identity while a streamed answer grows, and the folded thought's length is no
longer recounted per delta.
Refs #12232
MarkSessionViewed pre-checked the session sidecar with a plain unlocked
read. Concurrent views replace the sidecar, so on Windows that read can
meet it mid-replace (access denied, or torn JSON after the replace
fallback) and the view failed with a 500 instead of waiting it out. The
pre-check now uses the tear-tolerant read the other sidecar readers use.
Refs #12230
The kernel now records two optional facts in the session sidecar,
finished_at (stamped when a turn ends) and viewed_at (stamped when the
person opens the session). A session is unread iff finished_at is after
viewed_at; the sidebar tree and GET /sessions carry it as a typed
`unread` boolean. A turn the person cancelled is stamped as viewed.
Controller.MarkSessionViewed (POST /sessions/viewed) writes only the
session sidecar and takes no workspace or transcript lease. Sidecars
without the fields read as seen.
Refs #12230
A fold leaves the model with a digest, and the host re-projected only the
step ids of plan-seeded unfinished lists. A task list the model wrote itself
(no ids), a list with every item complete, the files changed and the checks
that passed all survived only as digest prose, which a weaker model reads as
work not yet done and repeats.
While a fold is installed the request now ends with one derived "Host
progress record": the canonical task list with statuses when no todo_write is
left in view, the files changed in the folded prefix, and the verification
commands whose latest outcome was a pass (marked when a file changed after
them). It is derived from tool receipts, bounded (30 files, 15 checks, with
the omitted count), memoised on the covered-prefix fingerprint, and ends with
the fold. Sessions that never folded carry nothing extra.
A live serve with no record is attached and recorded, launch no longer
deletes the port/pid files another serve published, and a live serve
whose provider source differs fails the connect with a coded error
instead of being terminated. Document what desktop exit does to the
remote runtime.
The lock directory sat at a shared, predictable /tmp path, so another
identity could create it first and every later run by this user failed
on os.Chmod with a bare EPERM.
Put it under the OS user's home (from the account database, so HOME and
TMPDIR overrides do not move it), as Windows already did. Keep the owner
uid check before the chmod as a backstop: it returns ErrLockDirWrongOwner,
naming the path and uid, without touching the directory. The /tmp name
is only a fallback when no home is recorded. The check is a no-op on
Windows; lockdir_owner_other.go is renamed lockdir_owner_windows.go.
Fixes#11372
Package segments made only of dots, leading dots, a leading underscore on
unscoped names and .tgz/.tar/.tar.gz names are no longer accepted by the
installsource and registry checks. parseGitHubRepoSource refuses dot-leading
owners and dot-only repos such as the one TrimSuffix(".git") leaves from
"...git".
Refs #11353
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
perf/tooltip.mjs looked the attach button up by its aria-label in each
language, and the English label it searched for, "Add attachment", is not
the shipped one ("Add an attachment"). The lookup found nothing and the loop
skipped on, so the four English attach combinations were never measured and
the guard stayed green.
Controls are now found by structure, data-action for refine and the attach
class for attach, and a control that cannot be found fails the guard rather
than being skipped. All 24 combinations (light/dark x zh/en x two widths x
three cases) are measured: 96 checks, where 80 ran before.
Refs #12156
The veil blurred the whole window behind it, which the compositor redoes every frame while anything beneath animates. A flat, more opaque scrim dims the same way for free.
GUIDE.md lists "Ctrl+L or /cls" as clearing only the visible transcript, and
1.x bound the key, but the 2.x terminal UI left Ctrl+L unbound. It now runs
the same clearDisplay as /cls: the conversation, session and the kernel's
task list stay, and a running turn is refused the same way. Being a key
rather than a typed command, it leaves a half-typed draft in the composer.
Refs #12144
The fold trigger compared only a local estimate against the threshold. That
estimate is characters times one tokens-per-character ratio, and the ratio is
not learned from requests that carry images or provider-side tool results, so a
session with screenshots can sit well under the real size and defer the fold.
The trigger now takes the larger of the local estimate and the provider's
reported prompt_tokens (cached tokens included) for the last request plus the
local estimate of what the visible view gained since. The anchor is dropped when
a fold changes the projection, when the transcript is swapped, and when the
usage is estimated, aggregated over retries, or padded by provider-run tools.
The hard ceiling and the overflow paths still read the local estimate alone, so
a provider count that no fold can lower never ends a turn.
The slash and @ completion menu and the /resume picker moved only with the
arrow keys, while 1.x also took Ctrl+N / Ctrl+P there and GUIDE.md and
CLI.md list both for these lists. The provider and quick pickers already
bind them. Both lists now take Ctrl+N as Down and Ctrl+P as Up; neither is
text, so a picker's filter is untouched.
Refs #12144
bash_output and wait reported a background job that later failed or timed
out only in their text, with no tool error. The activity panel and the
failure count read the host-set error, so a job that failed after it started
never turned red and was not counted, while the model was told it failed.
Both tools now return errJobFailed, naming the failed jobs, whenever a job
they report ended as failed (a background command's non-zero exit and its
timeout both end that way). The output is returned alongside, so the model
still reads it under the error line. A job that finished, was killed, was
interrupted or is still running is not a failure and the call stays clean.
Fixes#12201
POST /providers/probe built its refusal message from probe.Error(), which
carries the model-list response body verbatim. That body can echo the
request, and this route receives the key typed into the add-provider draft,
so a 401 that quoted the key sent it back in the refusal. A failure that was
not a ProbeError came back as an untyped 502 with the error's text.
The message now carries the probe reason and only what endpointDetail lets
through (one bounded printable line, the draft key and credential-shaped
text redacted), the same treatment /providers/check gives its detail. A
failure without a probe identity is a kernel fault and refuses as 500
provider.probe.failed without its text.
Fixes#12188
A phone using the web client got an empty Workbench when the agent opened a
page: useBrowserTabs returned early because the web host draws no native
views, so the tab list was never read. Read it on every host and, where the
page cannot be drawn, show its title and address as plain text with no
controls.
Opening a session pane read GET /checkpoints three times against a real
kernel: the mount effect read it, the turn-boundary effect read it again on
its own first run, and a third time when the first status named the session
path.
useCheckpointRefresh now owns every read outside a session reload: the mount
read, a move from one known session to another, and both turn edges. The
kernel opens a turn's checkpoint when the turn starts and fills it while the
turn runs, so the read at turn start stays and the rewind entry still appears
on a running turn's message. The first status naming the session the mount
read covered no longer reads again, so opening a pane costs one request.
A failed read is tried once more after a second, since no later edge is owed
to it, and a read that a newer one superseded is dropped instead of
overwriting the newer list.
Refs #12080
The save area sat at the end of a long form, so its state was out of sight
exactly when it mattered. The bar is now sticky, shared by the edit and add
forms, and holds the failure or not-applied reason beside a single persistent
status line with a dot for each phase (clean, unsaved, saving, saved, saved but
not applied, save refused). Ctrl/Cmd+S saves a pending draft.
A skill that declares paths: is held out of the per-turn listing, the
capability catalog, the slash_command listing and the unknown-name hint until
the session has touched a file its globs match. The set of touched files is
owned by the skill store; eligibility is memoised against it and dropped when
it changes, and the next turn after a match owes the listing again. The user's
/<name> and run_skill by exact name are not gated.
The shell card and the activity group decided failure a second time from
execution.state and exitCode, so state background_started (no error, no exit
code) rendered a red BACKGROUND_STARTED badge and counted as one failure.
The host already classifies every call once: err is set for failed,
timed-out, cancelled and refused calls. Read only that.
Verifying a model was one click per row, and the form blocked while any one
ran. The model header gets a Test enabled models button that runs the existing
per-model check over the ticked rows, two at a time, and stops as soon as the
draft changes or the form is left. The summary line counts rows still being
verified, and the per-row live regions are dropped so a run is announced once
through the summary.
The agent tells a PathObserver which workspace files a completed call named,
through the ReadTargeter and WritePathResolver capabilities its tool already
declares; failed, refused and hook-blocked calls feed nothing and sub-agents
are not observed. Swapping the conversation rebuilds the set from the
transcript. skillSet wires its PathHits in; the catalog does not read it yet.
A failed catalogue refresh surfaced under the form's footer and a silent
refresh that found nothing looked like a click that missed. The add form's
connection failure also rendered below its save button.
Refresh failures and outcomes (including 'no new models') now render under the
model header, verified rows are summed up in one status line over the ticked
models, and the add form's connection failure sits under the button that ran
it. Results clear when the address or key they depend on changes.
Skills may declare file globs in a paths: key. The registry now parses them
(rejecting negation, parent traversal and malformed globs into InvalidPaths,
which doctor reports) and a PathHits set records workspace files the host saw
touched, resolving symlinks so nothing outside the workspace counts. The set
is owned by skillSet; the catalog does not read it yet.
Leaving the edit form for another service, the Add form, another service's
rename, or a protocol switch dropped pending edits without a word.
EditConn reports the pending state it already computes; Providers holds it
and routes every way out of the form through one guard that asks to keep
editing or discard. A save in flight is not interrupted and not asked about,
and the question closes by itself once nothing is pending.
The edit form had no way to tell pending edits from stored ones: Save was
always enabled, Cancel was a remount in disguise, and a successful save
silently turned into the same form with nothing said.
Pending state is the fingerprint of what a save would send, compared with the
one taken at mount, so typing a value back or toggling a row off and on is
clean again. Save is disabled when clean, Cancel becomes Revert, and the footer
says whether there are unsaved changes, no changes, or that the save landed
once the list has been re-read.
Cause: POST /providers/check returned a free-text `error` assembled from the
probe error's message. The probe already knows whether a key was refused, a
path is missing, nothing listed chat models or the deadline expired, but that
identity was flattened into prose on the way out, so the settings page could
only print the sentence (which also echoed the endpoint body unredacted), and
the refresh button invented its own literal error when a check came back empty.
Blast radius: /providers/check on failure now answers 200 with
`{ok:false, code, params?, httpStatus?, detail?}` and no `error`. `code` is the
dotted identity the add flow's refusals already use (provider.probe.*), plus a
new provider.probe.timeout split out of unreachable; `params` carries only the
numeric facts the sentence needs; `detail` is the endpoint's own text through
the same redaction the model check uses. Success responses are unchanged.
catalog.ProbeError gains Status and Body, read from the typed fetch error, and
the status/code table is shared by the refusal and the finding.
Neighbouring behaviours tested: refused, forbidden, payment, rate limit, missing
path, upstream error, no chat models, unreachable, timeout, success, key
redaction in the endpoint text, bounded one-line detail, missing grant,
unknown name, and the add flow's probe codes all translated.
Why this layer: the probe owns the class; serve only projects it; the page
maps the code to words.
The refine button's tooltip carried the Ctrl+Shift+E shortcut, but nothing
pointed the button at it: it had no aria-describedby and the tooltip had no
id, so a screen reader announced only "Optimize prompt" and the keyboard
route stayed undiscoverable. The attach button beside it already describes
itself this way.
The tooltip now takes an id from useId and the button names it in
aria-describedby, matching the attach button.
Refs #12156
A settled render_chart call (direct or through use_capability) is re-validated
on the page, lifted out of the folded execution group, and drawn as a bar,
line or pie chart from the validated spec by our own React SVG components in
a lazy chunk. A running, refused or invalid call keeps the ordinary tool card.
The same card is rebuilt on reload from the stored call. Each chart offers an
accessible data table, keyboard-reachable points and no entrance motion.
A settled chart call (direct or through use_capability) now draws its title,
marks and rows as a capped, width-fitted table in the terminal, with numbers
right-aligned and the fold key opening the rest, instead of a line count.
chartspec.FromCall and Spec.Table/Describe are the single projection; the TUI
history record and frame merge now keep the resolved tool name, without which
neither a reopened session nor a live call could be told from any other
use_capability call.
A successful save remounted the editor synchronously while the list reload
was still in flight, so the new editor was seeded from the stale entry and
showed the pre-save values. onEdited now returns the reload, the editor
waits for it before remounting, and save keeps the form busy until then.
A steer sent while a stop had paused the queue was refused with inbox.paused
but its queued copy stayed behind, so retries piled up and all ran on resume.
Hold the item as a follow-up under the admission lock and answer with the
queued receipt that already carries paused=true. The pause itself is kept,
and send-now from the queue panel still refuses. The composer reads the
receipt, from the steer path and from an idle skill send, and says the queue
is paused, once, in the notice strip.
Part of #12153
Stopping a turn while the plan approval card is open left the plan in
AwaitingApproval with no card to answer it. Later messages then ran under
the planning barrier and no new card could open. Treat a cancelled or timed
out approval as a revise so the workflow can propose again.
Part of #12153
The refine and attach tooltips are children of their buttons. They inherited
the button's white-space: nowrap, so the hint spilled past the 220px box in
both locales, and the disabled refine button dimmed itself to 35% opacity,
which multiplied down onto the tooltip that explains the control.
The shared .studio-control-tip now wraps inside its max-width, and a disabled
host dims its glyph instead of itself.
Part of #12148
Cause: the settings page awaited setRole and only then re-read /roles. While a
turn, a question or a background job is active the kernel has already written
the value and answers 409 runtime.saved_while_running; run() treated that as a
failure, the re-read never ran, and the controlled select snapped back to
"follow the main model" although config.toml held the saved model. The banner
also called it "操作未完成".
Fix: the role row always re-reads /roles (promise finally), and the page's one
failure banner now distinguishes saved-but-not-applied (SAVED_NOT_APPLIED, the
set the provider edit form already uses) from a real failure, with the same
title, warn colour and status role.
Blast radius: Settings.tsx only. Every action routed through run() now shows
"已保存,尚未生效" instead of "操作未完成" when the kernel answers one of the
SAVED_NOT_APPLIED codes; other failures render as before. No kernel change.
Neighbouring behaviours tested: role saved while idle, role saved while busy
(value and notice), real refusal (alert, no saved notice), other role rows and
a reopened page after a busy save, default-model row refused while busy (stays
on the kernel's default; the kernel refuses before writing, so it is not the
same defect). Go: POST /roles mid-turn returns 409 runtime.saved_while_running
and GET /roles returns the saved value.
Why this layer: the page owns the re-read and the banner; the code identity
already comes from the kernel and the shared SAVED_NOT_APPLIED list.
Part of #12144.
Cause
- `/quit` and `/exit` are in the slash catalogue but the TUI had no handler,
so since #11969 they were sent to the model as ordinary messages.
- Every Ctrl+C on a running turn re-sent cancel, so a turn that would not
stop could not be left, while the footer says "Ctrl+C exits".
- Tab in the completion menu moved the highlight down (and did nothing
visible with one item); the guides say Tab / Enter accept.
Blast radius
- internal/frontend/tui: send(), menuKey, the Ctrl+C branches of onKey and
viKey, one new Options field. internal/frontend/cli passes the quit names
from the slash registry. docs/GUIDE.md and GUIDE.zh-CN.md rows for Ctrl+C,
Ctrl+D and /quit. No controller, wire or config change. The bare words
exit, quit and :q stay ordinary prompts.
- Tab history: the cycling was part of the first completion commit
(5ff9d7a0f) with no stated reason and no test; nothing later touched it.
Neighbouring behaviours tested
- Idle double Ctrl+C, Ctrl+D (idle and while running), double Esc opens
rewind, a second Esc does not quit, Esc then Ctrl+C quits, one cancel call
per first Ctrl+C, vi mode Ctrl+C, a finished cancelled turn needs two
presses again, Up/Down still move the menu, Enter accepts, Tab accepts
the highlighted row, file-mention directory descends, Tab without a menu
still asks the kernel, /quit while a turn runs is not queued.
Why this layer
- Quitting is a frontend lifecycle action with no controller verb, so it
lives in the TUI local-command switch beside /mouse; the names come from
the registry through Options rather than a second alias list. Both
interrupt paths share one helper so Esc and Ctrl+C agree on what
"stopping" means. The unreachable shift+tab case in menuKey is removed.
When the kernel refuses browser.open, the workbench stored the reason in
`failed` but only drew it inside the file-tab branch. Opening the column
or pressing + with no page left the pane on its empty state, "choose a
file on the right, or open a browser", with no error.
Cause: `failed` was rendered only when the active surface is a file, while
the browser open paths (column opening with no agent tab, the + button)
also write it. The kernel also answered every failure with the generic
browser.open_failed, whose wording needs an {error} param it never sent,
so the typed engine_missing / engine_failed / profile_busy causes were
lost. Separately, `.workbench-error` used --danger, a token no stylesheet
defines, so such errors drew in the body colour.
Blast radius: WorkbenchPanel, one CSS declaration, serve's browser open
refusal and the kernel.ts / en_kernel.ts tables. The alert renders for any
non-file surface; file tabs keep their own copy and are unchanged.
`failed` is cleared when a tab is picked and when the active tab closes.
POST /browser/open now answers browser.engine_missing, browser.engine_failed
or browser.profile_busy for those causes (same 400, same params, plus the
detail in params.error) and browser.open_failed for the rest. The colour
fix also recolours the file-tab and explorer errors.
Neighbouring behaviours tested: the three codes and the generic one through
the real SsePort decoding path, on both open paths, asserting the exact
text and no leftover placeholder; the reason clearing on picking a file
and on a later successful open; a file read error staying single on its
tab and going away with it; serve end to end with real launch failures;
the refusal-code parity test; the whole WorkbenchPanel suite and tsc.
Why this layer: the pane owns the state and the draw; the kernel boundary
in serve is where the typed cause becomes a code, and internal/platform/
browser is not touched.
Fixes#12139
Add internal/base/scratch: a temp directory claimed by a long-lived
exclusive .owner.lock (internal/base/filelock). Remove releases the lock
first and retries sharing violations for up to 2 s. Sweep/SweepOnce remove
only non-symlink directories past a one-minute grace whose lock can be
taken, so a live owner is never touched. The jobs manager now creates its
reasonix-jobs-* fallback root through it, which also sweeps roots left by
earlier killed kernels.
Part of #12142
In 1.x, "/resume <n>" switches straight to the nth saved session. The Studio
TUI ignored the number and always opened the picker, the known gap left in
the CLI migration tracking issue.
An indexed /resume now resolves the session by its row in the same list the
picker numbers and resumes it, with the same refusals the picker gives: a
running turn, the session already open, or an index outside the list. A bare
/resume (or a non-numeric argument) still opens the picker.
The index lookup shared by /takeover <n> and /rename <n> <title> moves into
one helper so the three commands cannot drift apart.
Refs #11493
The feedback dialog is fixed to a 720px column and thread bubbles are capped
at 560px, so a long maintainer reply on a wide window reads as a narrow strip
that needs constant scrolling.
Add a "Fill the window" toggle next to Close. While pressed, the dialog takes
the whole veil and thread bubbles widen to 92% of the column; pressing it
again returns to the reading column. The toggle is a view-only action
(feedback.expand) and the state lives only for the open dialog.
Refs #11575
The reference-solution half covered only the e2e, train and verification-stress
corpora, so three committed corpora with the same tasks/ layout carried no
evidence that any attempt can score: a grader that can never pass is
indistinguishable from a task with no solution, and a corpus of those teaches
that nothing is ever accepted.
Their pristine seeds already fail, checked across all eleven tasks, so this adds
the reference solutions the guard needs: one ANSWER.md each for the three fleet
tasks, and the non-positive-skipping calc.py the eight project-check graders
assert. The e2e suite stays the single declared exception, named in the skip.
Refs #11852.
Prompt refine and commit-message drafting built their provider with effort forced to "disabled". Generic OpenAI-compatible endpoints refuse that at construction, so the side model was nil and the user saw "no usable model" while chat worked. sideProvider retries only on provider.ErrEffortRefused, with the lowest effort the entry's vocabulary lists (automatic depth when it lists none); other construction errors are not retried and are logged at warn.
When a summary fails under the pressure trigger the retry is held, and
while it is held and compaction is due nothing reached the user. The
agent now emits a coded notice, compact_held, once per turn and cause,
with the failure's code as detail. The desktop and the TUI word it from
the code in the UI language; a code a build cannot word keeps the
kernel's English.
A transient summary failure held the retry until the input grew by an
eighth of the trigger. Recorded near 105% of the trigger that release
point is the window itself, so the next request went out uncompacted.
The step is now capped at half of the room left below the hard ceiling,
so the hold always lifts before it.
A relay that refuses an oversized request with a 400 and no error code
was not recognised as an overflow, so recovery never ran. A 400 or 413
whose request our own estimate puts over the window now takes the same
fold-and-replay path, judged by status and size, never by wording.
A session row was selected only once POST /runtimes (or /resume), GET /runtimes
and GET /tree had all returned, so every switch to a session without a pane
waited out the kernel's pane assembly with nothing but a dimmed row to show for it.
The window now owns which session is being opened (useSessionSwitch). The row
reads selected and busy from that state as soon as it is clicked, the active
pane shows aria-busy, a ticket orders concurrent opens so the last click wins
and a refusal from a superseded open changes nothing, and a failed open drops
the selection back to the pane that is still in front while the error is shown.
Opening moves out of App into useOpenPane so every caller shares the one path.
The pane burst after an open now asks for /runtimes and /tree through one
single-flight read instead of one per caller.
/status called config.Load() on every poll, which re-parses every
installed plugin package under a process-wide lock. The controller now
carries the model entry boot resolved and /status reads effort, vision
and the model ref from it. The provider label still comes from the user
file because a rename rebuilds nothing.
Refs #12055
Discovery read every directory-layout skill's references/ and scripts/
to build a Body that a catalog never uses, so opening a pane cost
O(bytes in the skill library). Listing now keeps only SKILL.md; Read,
ReadSlash, Prepare and the capability digest complete the skill when it
is invoked or inspected.
Refs #12055
A summary request was cut off at a fixed 90 s of wall time, so a local model
that was still streaming steadily failed every /compress with summary_timeout.
Bound the silence between streamed chunks instead (6 minutes, one minute past
the providers' own 300 s stream idle watchdog), and add a 30 minute overall
ceiling with its own typed code summary_ceiling so a trickling model cannot
hold the turn. Both failures stay transient: a later attempt may retry once
the input has grown. The notices state which bound fired and its length.
GET /skills resolved each skill's activation through Controller.SkillEnabled,
and every call re-ran config.Load(), which walks all installed plugin
packages. Resolve activation once per request via Controller.SkillActivation.
Refs #12055, #12051
Go's default User-Agent is answered with 403 by edge bot protection on
datacenter networks. Catalog and manifest reads now use the Studio update
identity shared with artifact downloads, the relay websocket handshake uses
the account client's identity, and feedback, telemetry, crash and account
requests fall back to the shared client identity when none is set.
The pane polled /status every 250ms with no in-flight guard. When the kernel
answered in 13-16s the unanswered reads filled the browser's per-origin
connection pool and the approve, plan-decision and answer POSTs queued behind
them.
Idempotent reads now share one request per path with a single trailing
re-read, the pane schedules its next poll when the previous read settles, and
decision submits get a bounded wait that fails with a typed KernelBusyError.
The retry status line showed "connection broke before the response headers"
for every retried failure, including 502/429 statuses, did not say what the
attempt was waiting for or when it counts as unanswered, and its seconds
counter ran from the first notice of the whole stall instead of per attempt.
The retrying event now carries a typed failure class (connection_closed,
timeout, upstream_status with the HTTP status, stream_idle, upstream_error),
the backoff before the next attempt, and the header wait of the coming
attempt. The producers set them: SendWithRetry knows the class, the adapters
pass their idle_timeout_seconds window, and the stream replay path maps the
stream-interrupt reason. The window words each class and phase (backoff
countdown, then a wait that restarts per attempt) from those fields, never
from text. The CLI machine record carries the same fields.
Provider settings gain one advanced row for idle_timeout_seconds (1..32767,
empty is the default), validated in the form and by the kernel's provider
edit endpoint.
Fixes#12046
The host book has no per-machine state: every entry is dialed on demand and every entry is
listed in the rail. A machine that is temporarily off the network, or one kept for the
occasional job, cannot be marked as such - the only lever is removing it, which takes its
folders and its forward rules with it.
- RemoteHostEntry gains disabled, written as "disabled" rather than "enabled" so an entry
written before the field existed still means what it always did, plus IsEnabled(); the
renderer writes the key only when it is set.
- One switch per row decides whether the machine is dialed, and that is all it decides:
live panes keep running and the rail still lists the machine, because a machine being
off the network says nothing about what this window should show. Whether this window
lists it stays a separate preference, and it is an immediate row action next to the
row's other actions rather than a control inside the edit form, which saves on submit.
- remoteHostDialable is the single answer to whether a machine may be reached: POST
/remotes/open refuses a disabled one with remote.disabled before dialing, and remoteLink
(the tree and directory reads, and removing a far session) asks the same question, so a
disabled machine is not reached for at all. A book that cannot be read is a refusal
rather than a dial.
- GET /remotes reports disabled and the edit endpoint carries it.
Project every string and list of an install plan through one bounded
projection shared by all frontends: names, paths, commands and arguments
render hidden characters as visible escapes, prose has them removed, and
each field, list and the action count has a cap. A cut ends the field with
an ellipsis and sets previewTruncated on the action and the plan. The planId
still digests the complete unprojected plan, and the market ledger records
the unprojected paths. Installed-package detail views (inventory, record,
runtime, warnings) use the same limits from the shared text utility.
#11897 stopped a session's model switch from rewriting default_model, which left the
settings row reading status.modelRef - the model the session in front is running - so
switching models inside a conversation still moved that row while the file it claims to
describe did not change.
The row now shows the entry the model catalogue marks as default, falling back to the
session's model when the catalogue carries no default, and the column beside it follows
the same value. `main` stays the session's model for the vendor-protocol inference this
component does.
Tested: with a catalogue default the row shows it whatever the session runs; without one
it falls back to the session's model; the settings-catalogue, ModelUsage and i18n gates.
The interface scale was declared four ways: the config comment said
0.8..1.6, the save clamped to 0.7..1.8, the slider ran 0.8..2.5 and the
presets covered 0.9..1.3. Dragging the slider to 2.0 showed 2.0 while the
kernel stored 1.8.
config.ZoomMin/ZoomMax/ZoomStep are now the only declaration. The save
clamp, the rendered config comment and the range /appearance announces
(mirrored to the page by wire-parity) all read them, and the slider is
drawn from the announced range instead of carrying its own. A stored
value outside the range reads as the nearest bound but is not rewritten
unless the user changes the zoom.
Ctrl/Cmd + '=' / '+' / '-' / '0' (read by key or by key position, so
shifted and non-QWERTY layouts work) step the interface scale through the
named presets (range ends outside them) and reset to standard, saving
through the same appearance setting as the Appearance control. Chromium
page zoom is not used.
The range is 0.8..1.8: 1.8 was the largest value the old save could
store, so nobody loses headroom. The keys exist only inside the desktop
shell; a browser tab keeps its own page zoom.
Fixes#12032
Part of #11962
A stable studio-vX.Y.Z that is the highest stable studio tag is created with
--latest and without --prerelease. Candidates (-preview.N, -rc.N) keep
--prerelease --latest=false, and recovering an older stable tag does not take
latest back. The decision lives in scripts/studio-release-flags.sh and reads
the tag grammar, not wording.
The GitHub release no longer carries a root latest.json. The 1.x desktop
updaters fall back to releases/latest/download/latest.json, and builds up to
1.18 do not validate the manifest and share Studio's minisign key, so a
Studio manifest there would offer them a Studio package. The file stays in
the R2 mirror under <tag>/, which is the only place Studio reads it. The
release now has 21 assets.
Adds a flags-script test and a workflow contract test, and updates the
runbook, READMEs, MIGRATING and ROADMAP.
A deny or ask rule such as Bash(rm:*) compared only the words the model wrote,
so `env X=1 rm`, `sudo rm`, `command rm`, `nohup rm`, `nice rm`, `time rm` and
`/bin/rm` were judged by fallback mode instead of the rule.
shellparse.PeelWrappers reads one simple command from the shell AST and
follows a fixed set of wrappers (env, sudo, doas, command, builtin, exec,
nohup, time, nice) down to the program they run, naming a path-spelled program
by its base name. Deny and ask matching test every layer; allow and session
rules are unchanged. A wrapper whose target cannot be named statically needs a
human instead of falling through to the mode.
apply() wrote every pack token, and the ink variables even with no pack,
as inline custom properties on <html>, so any author stylesheet needed
!important to win. The values now live in one rule of a <style
id="pack-theme"> created after the built-in sheets, written through the
CSSOM so a rejected value is dropped. The contrast tiers carry the inks in
the stylesheet, with --ghost on --faint's value, and perf/tiers.mjs holds
each tier to the legibility users already had.
A Claude Code Mod declares "modules" in hooks/hooks.json instead of a
"hooks" map. The importer returned early on an empty hook map, so the
modules were dropped silently and a Mod with skills previewed as full.
The model list collapsed entries by endpoint + model, so two providers
sharing a base URL with different API keys listed each model once and
the second account's refs vanished from the picker.
TestInstancesDifferBetweenRuns instantiated every task twice and failed
whenever two independent draws collided. Three tasks answer with a bounded
number (i70 quota 1200..9800, i47 lease 15..300, i112 lease 20..420), so a
run fails with probability 1/8601 + 1/286 + 1/401, about 0.61%.
liveRand now delegates to liveRandFrom(io.Reader). The test asserts the
invariant at the seed: two live generators do not share a stream, the same
entropy yields identical instances and different entropy yields different
values.
The walk tolerated only not-exist, but a directory being removed on Windows
fails to open with a sharing violation or access denied. Those errors now
count as vanished only when a re-stat shows the path gone.
Cause: the per-model check classified any 4xx it could not name as
unknown/rejected and the row printed only "request refused, not confirmed".
The kernel already carried httpStatus; the settings UI dropped it, and the
endpoint's own error text was never sent, so a local Ollama 404 (base URL
without /v1, model id without its tag), a 400 for a model that cannot take
tools, and a missing key on a System One entry all read identically.
Blast radius: POST /providers/check/model gains an optional `detail` field:
the endpoint's error text as one printable line. It is built in this order:
control characters and ANSI sequences replaced, the probe key masked in raw,
URL-escaped and JSON-escaped form (skipped for keys under 8 characters),
secrets.RedactCredentials, then cut to 300 runes. Folding and the key
replacement see the whole text; only the pattern scrub is bounded (8 KiB cut
on a word boundary, after folding). The model row shows an HTTP badge and the text inline. Status and
reason are unchanged and never derived from the text.
Neighbouring behaviours tested: Ollama-shaped upstream (correct, base without
/v1, tagless id, System One kind); 400/404/405/422 classification; detail
equals the upstream text, masks raw/escaped/plain keys, Bearer, ?token=,
Cookie, neutralises newlines/ANSI/NUL, cuts after redaction, cuts at the scan cap with the key split at every offset, with and
without leading whitespace, bounds a 4 MB body, leaves short keys alone (each mutation-checked); add and edit flows
share one fact helper; full ui vitest suite and the serve package.
Why this layer: the serve handler owns the typed result and the row is the only
renderer of it; classification stays where the failure is first known.
POST /browser/open answers 400 when the navigation fails even though the
window's tab already exists, and the link handler read every refusal as
"nothing opened" and sent the same address to the machine's browser.
The refusal now names the tab it left behind and the handler only falls
back when there is none.
A pane adopted at launch is not a folder anyone picked, so nothing listed it
once a later launch opened another one. Its conversations stayed on disk with
no sidebar row to open them from. The folder is now listed behind the existing
ones when the pane's first turn mints its session path.
A queued line taken back could fail with a spurious error on a repeated click,
leave a pending transcript row when its receipt landed after the withdrawal,
and reappear in the panel when an older queue pull answered last.
A truncated, timed-out or failed summary request blocked automatic
compaction for the whole generation, so context kept growing past the
fold trigger with no further attempt. Block release now reads the typed
failure code: transient request failures are released after the input
grows by one retry step; every other class stays blocked.
The digest request inherited the session's reasoning effort, so reasoning
tokens spent the same 16384-token cap the digest had to fit in and a
finish_reason=length dropped the whole summary. It now carries
EffortOverride=low; adapters apply it only when the endpoint's own effort
vocabulary admits it, so endpoints without a low tier keep their configured
depth.
Refs #11970
/compact notices were raw English sentences built from err.Error(), and an
aborted card with no code read "compaction done / folded nothing". Add
compacted/compact_declined/compact_failed notice codes (detail = the
compaction code), give cancellation, unclassified failures and a busy
session their own codes, and word them in the desktop and TUI catalogues.
Refs #11970
The terminal UI asked the kernel to refuse any slash line the registry does
not answer, so a typed path or half-remembered command dead-ended with
"unknown command". 1.x sends it as an ordinary message and says so (#5756).
The TUI client no longer sets refuseUnknownSlash; the controller's existing
default path runs the line through the normal turn pipeline. That notice is
now localised (en/zh/zh-TW) and carries the unknown_command code.
A built-in named first is that built-in whatever follows it, as in 1.x: the
kernel matches /context, /new and /clear by first word, and the TUI does the
same for /help, /clear, /setup, /version, /mouse, /resume and its display
commands, so none of them is sent as prose when given arguments.
Refs #11493, #5756
With no API key the connection panel opened silently and a typed message went
to the provider, which answered 401 with a pointer to a shell command. The
panel now names the model that lacks a key, and /submit refuses a turn for it
with provider.key_missing before anything is sent. The TUI returns the line to
the composer and points at /setup.
The usage range test assigned the process-global time.Local while goroutines from earlier tests still read it, so the race detector failed the package. usageWindow now delegates to usageWindowIn, which takes the zone trailing windows count days in; the test passes a fixed zone and never touches the global.
A provider whose natural key slot is taken gets REASONIX_CONNECTION_<id>_KEY. Collect wrote that provider and its key, but restore validation denied every REASONIX_ name, so the backup sealed fine and then failed to open as malformed.
Settings and feedback are answered by a pane's kernel. With no pane open, feedback silently did nothing and the settings refusal never cleared. Opening either now starts a session in the current folder, a refusal is shown for both, and it is withdrawn once a folder or pane exists. The cloud-backup button now states what it is waiting for.
Preserve ACP frame-order assertions while skipping only MCP status updates,
and use t.Context in the serve regression test.
Report servers requiring launch approval as pending rather than failed in
controller, ACP and /mcp snapshots. Keep pending rows disabled without a retry
action while retaining their recorded transport and sanitized error.
Document connecting and awaiting-approval notifications, style the pending
indicator in both themes, and derive frontend state coverage from Go constants.
Add controller, endpoint, ACP and row-rendering regression coverage.
- Map the new `pending` state in the desktop panel with an i18n label. A
repository-declared server nobody has answered for is off, but not because
the user switched it off, and the row rendered the raw state string.
- Announce what actually happened. RecordFailure and
RecordLaunchApprovalRequired now share one store path and name their own
state, so a project MCP awaiting authorization stops reporting a failed
connection.
- Correct the MCPSurfaceReady contract: it fires on every change to what /mcp
answers, not once per server. Audited every consumer before changing it —
the ACP sink republishes a full snapshot, the serve broadcaster numbers the
frame and the SPA refetches /mcp, the headless stream passes it through — so
repeats are harmless and the documentation was the stale part.
- Keep the spawn claim beside the rest of the deferred-spawn bookkeeping
instead of a file of its own.
- Add the /mcp endpoint regression test the refactor was missing (live
transport and source, recorded status code, in-flight spawn, cached
description and tools), and a panel test tying every state label to the
English catalogue.
SetCredential and SetCredentialIfRevision built a KEY=value line and parsed it
with the dotenv reader, so ${...} was expanded and wrapping quotes stripped
before the key was ever written. The writer also left a value containing $
bare, which the reader then expanded. Store the assignment directly and pick
the first encoding the reader returns unchanged; a value no encoding can hold
(for example a trailing backslash next to a quote or $) is refused before
anything is written, so one entry can never make the whole file unreadable.
Adds GET/POST /display-currency over the controller's new
DisplayCurrencySettings/SaveDisplayCurrency (shared with /currency), a
Settings > Usage control, and makes the quote sink follow the
display_currency notice so rounds after a change are priced in it.
Picking a 1.x data folder only looked at <root>/sessions, so per-project
sessions, sessions-v4 and desktop-sessions-v5/by-id imported nothing and the
UI reported no old sessions.
render_chart validates a chartspec v1 spec with chartspec.Parse and answers
with a chart_id and the deterministic text summary, never the rows. It is a
registered built-in left out of the provider allow list, so it is found and
called through use_capability and the cache-stable prefix is unchanged. The
spec persists in the call arguments the session already stores (#11564).
cli-channels and cli-pointer both waited only for cli-tag, so the cask could be
pushed while the v<version> release holding its archives was still being
created, and stayed pointing at nothing if cli-pointer then failed. The cask
step also overwrote the tap whatever its version, so a workflow_dispatch of an
older tag moved Homebrew backwards; it now skips a tap that is ahead and fails
closed when the tap's version cannot be read.
Leaf package internal/contract/chartspec: spec types for bar/line/pie, a
strict Parse (size cap first, unknown fields refused, scalar cells only,
unsafe integers refused), typed refusals with dotted codes and caps, and a
deterministic Summary that quotes cell data. No consumer yet (#11564).
EditConn titled every outcome "not saved" at warn level, so a write that
landed but was not applied to the live session read as a failure, and a real
failure read as a soft notice. Split by outcome: error alert for a refused
save, status note for saved-not-applied, own title for a catalog refresh.
The archives that reasonix upgrade, the Homebrew cask and the GitHub release
install were linked with main.version only, so they reported no git_commit or
build_time_utc and the embedded product docs identified themselves as dev. The
npm packages of the same release were linked with all of them.
CostQuote carries a rate band for a rate confirmed as the vendor's own
schedule, aggregates fold it to peak, off-peak or mixed (none when any call
has no schedule), and the TUI prints it beside the cost as 1.x does.
A headless run in a folder with no trust decision refused every edit and
command and exited 0, so scripts that relied on 1.x defaults looked
successful. It now exits 4 (3 under --fail-on-unverified); the gate and
what it allows are unchanged. docs/MIGRATING.md gains the 1.x differences:
trust prompt, untrusted-folder runs, compact-ratio range, env vars.
Refs #11493
The panel dropped a list as soon as every item was completed, so the final
N/N never showed. It now stays through the ask panel and the idle prompt and
goes when the next turn begins; a rebuilt screen (resume, rewind) still does
not draw a list that finished before it was opened.
Adds a typed Commits port on the controller: ProposeCommit reads the staged
index through gitcmd, frozen as a tree object, and asks the session's model
for a conventional-commit message in a request of its own (prefix untouched,
billed as commit-message). CommitStaged writes the reviewed tree with
commit-tree and moves HEAD only if it has not moved, so an index that changes
after confirmation cannot enter the commit. Files named as secret holders or
whose added lines carry a private key are withheld from the model and need an
acknowledgement. Hooks and signing never run. Nothing pushes.
Refs #11518
- Add the pre-5.3 GLM catalogs to shippedPresets so installs holding the
old list are moved forward, plus upgrade tests (old shape upgrades, a
curated catalog stays untouched).
- Declare the per-model ladder once in provider.ZhipuEffortContract and
read it from the /effort menu, the normalizer and the request builder,
instead of repeating the model lists across three switches.
- Cite Zhipu's Core Parameters reference for the documented levels
(5.2's none/minimal/low/medium/xhigh folds, 5.3's forced thinking).
- Add a boot.Build effect test at the provider-request boundary covering
the stored enabled/disabled -> EffortDisplay -> request path.
- Surface the disabled->low landing for GLM-5.3 in the /effort menu and
document the 1M context window the added models reuse.
CodeQL's go/command-injection models regexp matches as barriers only when the
match is the branch condition dominating each use. screen() folded the match
into an || condition and the -C directory skipped it, so alert 1014 kept its
flows through screen() and Args.
- keep typed field details for days and explicit ranges
- reject from/to combined with days
- treat explicit dates as zone-less calendar days
- relocate and extend usage range tests
rebuildFailed collapsed the busy refusal into runtime.rebuild_failed with
the English sentence as detail, so the compaction, shell, sandbox,
permission, browser and MCP-load saves told a zh reader the write had
failed. It now answers runtime.saved_while_running, which the UI renders.
A bare https://host passed the add-provider probe because {base}/v1/models
answered, while chat posts to the saved base + /chat/completions and failed.
The probe now reports the base that was proven (typed address completed with
/v1 only when the listing answered there alone, openai-style kinds only), and
add, onboarding and edit-refresh adopt it into the address field before save.
F5 and Ctrl/Cmd+R reloaded the whole Studio UI from any focus, so a page
or browser-pane user pressing them lost the window state. The app window
now reloads only on Ctrl/Cmd+Shift+R (white-screen recovery, crash reload
unchanged); the browser pane reloads its own page on F5 and Ctrl/Cmd+R and
ignores the cache on the shifted chord.
POST /model persisted every switch as the user's default model, so changing
the model inside one conversation silently changed what every new session and
the next launch started on. The typed /model command, the terminal UI and ACP
never did this; only the HTTP picker path did.
The request now carries an explicit `default` flag. The Settings default-model
row and the first-run model choice send it; the composer picker does not, so
it changes only that session. A pane resolving through the hub's resolver
still never writes this machine's config.
One structural rule (secrets.RedactEndpoint / CredentialKey / RedactArgs)
replaces mcpsetup.RedactURL's query-only masking. Userinfo and fragments are
dropped or masked, query and fragment keys are decoded before classification,
unparseable endpoints render as <redacted>, and transport diagnostics keep
error identity while omitting peer-supplied prose.
Operational values (install, approval fingerprints, persistence) are untouched;
preview and plan responses add separate display fields that the desktop mirror
and confirmation UI render.
Rebased onto the launcher-aware NameFromArgv: names derive from the original
argv but never from a value the display projection masked, so build-flag values
such as `go run -exec` still resolve. Install classification reads the typed
HTTP status instead of sniffing the server name out of the message.
Fixes#11785
The primary button is focused from a passive effect that runs after the
element is committed, so asserting document.activeElement right after a
findBy* resolves races that effect. Use waitFor, as settings-layers does.
testenv.TempDir retries RemoveAll for two seconds when a file stays held.
On Windows the usage and history SQLite catalogs, which the shared helpers
left open, held a file under nearly every test's directory, so each of those
tests paid the full retry budget at teardown. Closing the catalogs from the
helper's own cleanup, which runs before the removal, makes it converge on
the first attempt.
A successful Windows bash probe is remembered per executable identity (path, size, mtime) for the life of the process; failures are never remembered. Every boot.Build used to spawn Git bash for it.
agent.Options.WorkspaceScanLimit (zero keeps the 50,000 production bound) is threaded from boot.Options. The incomplete-prose-scan effect test now sets it to 64 and writes 65 files instead of 50,001.
The deadline starts at launch, so with a real interpreter the test measured startup time. Under load Git bash needed more than 500 ms and the pre-deadline write never landed.
TestRememberPermissionRuleSerializesConcurrentWriters queued 32 writers on a
lock that waits 5s, so a slow Windows filesystem ran the last waiters out of
time (lock timeout at 5.08s). The cross-process variant had the same shape:
each helper waited a fixed 5s for the others to spawn. Eight writers still
interleave, and the readiness waits now read the shared hang budget.
The deadline is armed at launch, so a 500ms budget races interpreter startup:
on a loaded Windows runner PowerShell had not printed anything when it
fired, and the test reported the pre-deadline write as lost. PowerShell now
gets a 10s deadline named for what it has to outlast; POSIX shells keep 500ms.
Hang-catching waits in the control tests were hard-coded to 2, 3, 5 or 10
seconds. A whole turn through the real agent loop can exceed that on a
loaded Windows runner, so TestTurnOutcomeClassifiesPerseverationPause failed
with 'timed out waiting for TurnDone' on a run that passed on rerun. They now
read testenv.Budget(t), which is the single named bound for a wait that only
exists to report a hang.
cloudPresence kept a monotonic counter, so every reconnect of the same phone
took the next number. Allocate the lowest ordinal not held by a live
connection instead; it stays fixed for the connection's lifetime.
The wide-workspace effect test compared total goroutine growth against a
magic 64, which host background work (sqlite watchers in the history
catalog) pushed past under load. Count only goroutines inside the walkers
and bound them by pool size times concurrent walk kinds; assert none
survive the turn.
A fold that was refused or failed ended with an aborted card reading
"folded nothing" and a blocked receipt whose code was empty; the cause lived
only in a sentence. Checkpoint rejections now carry a code per rule, summary
failures are classified by sentinel (truncated, timeout, input too large,
context changed, provider failure), and the code rides both the receipt and
the aborted compaction frame. The Studio card shows a localized cause, and
keeps its before/after token pair on one line when the row wraps.
The Windows runner's temp dir is an 8.3 short name. The grant target for an
ambiguous path is resolved to the long form, so the expectation built from the
unresolved root never matched.
The regression held row and button references across the refetch that
PackageList.submit now triggers after a successful review submission, and
its deferred submitMarket fixtures never updated the inventory myMarket
returns. Query live rows after each reload and delegate to the mock's real
submitMarket so the reloaded inventory reflects the new status.
A tilde inside a Windows 8.3 short-name segment (RUNNER~1) of a drive-letter
path is an ordinary character; any other tilde can expand to a home
directory and stays refused as non-literal. Unescaped braces are refused
too, since a brace word expands to several targets.
Also give the PowerShell delete-analysis subprocess test a hang guard
instead of a 15 s latency assertion that fails on loaded runners.
The reference-solution guard fired only for the train corpus. For every other corpus a missing solution/ was a silent skip, so a grader that can never pass stayed indistinguishable from a task with no solution - the case the rule above it exists to prevent. #11380 supplied the ten vs-pos solutions; this extends the guard to the whole verification-stress corpus and supplies the ten tasks it was still skipping.
The e2e suite keeps the skip, and the skip message now says why: its handwritten tasks do not commit a reference solution.
Verified: go test ./cmd/e2ebench -run TestCorpusGradersPassTheReferenceSolution is green across e2e, verification-stress and train. Removing any single solution/ fails the guard at corpus_test.go:254 (checked by hand for vs-neu-cfgkey), so the added solutions are what the guard grades.
A relay that rejected the continuation field with a message the client did not recognise surfaced the HTTP 400 to the user, and the stored response id was kept so the next turn sent the same rejected request. Any HTTP 400 on a request that carried previous_response_id now retries once with the full history; a completed recovery stops sending the field for the client's lifetime, a failed one surfaces its own error naming the attempt.
Revision after the security review: ordinary commands with dynamic executable names, non-recursive deletes and non-delete wrappers launch again; each refusal carries its own cause and remedy; bounded compound sequences with literal targets inside the granted roots are allowed; default-parameter, pipeline, alias, .NET, find, xargs, cmd and static interpreter payload deletes are judged; symlink components are resolved before dot-dot cleaning; the session temp root is a granted scope; the PowerShell parser is reused across calls; bilingual copy for the new codes.
Doctor now validates declared agent roots fatally, which the warning test added by the root-diagnostics change did not expect, so the two merged changes failed together. The warnings are printed first and the root rejection keeps its exit status.
Approval alone did not stop a recursive delete: in unattended Auto and YOLO a PowerShell sequence whose first assignment failed non-terminally went on to delete the real home directory. A pre-launch gate now parses the command (bash AST, PowerShell parser run as data) and refuses recursive deletes whose target is the home directory, a drive or filesystem root, a workspace ancestor, an expansion, variable or glob, or outside the workspace and granted roots, with a typed shell.destructive_target code; parser failure is shell.analysis_unknown. The gate runs after approval and before every launch route.
A write to a markdown or text file in a workspace with no declared or captured checks still added a stale_verification debt and, in Balanced, a readiness floor demanding a verification command. When every proven mutation is a supporting-role path and nothing declares or captured a check, neither applies. Mixed, opaque, unproven or partially observed effects and declared checks keep their behaviour.
The sidebar row and the account panel disagreed when a token was present but the identity lookup failed: the row showed a signed-in state with a question-mark avatar while the panel drew the sign-in form and hid the error. Both now render the signed-in state, the identity error, a retry and confirmed sign-out.
The record panel was rendered after the whole active-turns list, so with a few
hundred rows it sat thousands of pixels below the click. It now renders inside
the clicked row's list item (or under the signal track for a signal), the same
item stays mounted, and clicking again closes it.
The session filter row gave four equal cells to labels with counts, so at the
old 176px floor the labels collided; the new-session and search rows wrapped
around their shortcut hints. Raise the column floor to 232px, let the filter
cells size to their labels, ellipsize labels, and drop counts and shortcut
hints below a container width.
The session and project menus wrote clientX/clientY into left/top, but the
interface scale is CSS zoom, so a fixed element's left/top is multiplied by the
zoom on its way in. Both now go through pinToViewport, which divides by the
zoom and clamps the measured box inside the viewport. The session menu's
max-height is divided by the zoom as well.
The official DeepSeek entry's model list migrates the retired
deepseek-v4-flash name to deepseek-flash, and qualified refs move with it,
but a bare default_model = "deepseek-v4-flash" (the shape a shared 1.x/Studio
config carries) was left behind. It then resolved to nothing, and because an
unnamed boot preserves an unknown default for its error, the Studio host's
only pane failed to build and the host exited 1 before its handshake.
- config: migrate a bare retired flash ref (default_model and every other
model-ref field) to the official entry's successor, unless another
provider still serves that name.
- boot: a new OpenOnFallbackModel option, set only by the Studio host and
hub panes, lets an unnamed build pass over an unknown non-plugin default
like a keyless one, open on the first configured chat model, and report
default_model_unavailable. The config file is not written. run, the TUI
and every other build keep the actionable unknown-model error and never
move to another provider on their own.
- serve: removing the provider in use moves the conversation to the startup
model when the saved default names nothing, instead of answering 500
after the removal; session titles follow the same startup model.
A background sub-agent keeps emitting reasonix.subagent.* progress after the
dispatching call has returned its job id. Folding those frames into the call's
card re-opened it, renamed it and its execution, and nothing closed it again.
Progress now lands in per-call phase state beside the cards; the Subagents
panel reads a child as running while its call is open or its last phase is not
terminal.
Take back cancelled the entry and dropped its text, so the label promised a
retrieval nothing performed. The body is now read before the entry is given
up and appended to the composer, under whatever is already typed. A line whose
body cannot be read stays queued rather than being lost.
The header chip counted client-side pending rows, which a delivery reported
before its HTTP receipt left pending forever, so it named a line the queue
panel (the only place to act on one) no longer showed. It now counts the
kernel's waiting guidance through the same predicate the panel filters by,
and a receipt that arrives after its delivery no longer re-opens the line.
The terminal UI opens a Configure connection panel when the session owes a
key, and /setup or /auth opens it at any time. The key is tested without
being stored, saved to the credential store only, and applied to the running
session when it belongs to the active connection; config.toml is untouched.
The Studio terminal UI answers the 1.x commands that read or steer the
kernel's own state, with 1.x argument shapes and output. Queue verbs and
steer go through the existing inbox routes, so the controller still owns
admission and ordering. /export is a controller capability: the file is
created exclusively and never through a symlink. /takeover resumes the
target without taking a session from another process.
foldTool looked for a call's parent only among top-level cards, so a
worker's own calls, whose parent is the per-worker task card nested in the
fleet group, fell through to the main list.
Pastes of five lines or a thousand characters become [Pasted text #N · M lines],
counted whatever row ending the terminal uses, and expand on send into the
labelled Begin/End block 1.x sends. Numbering continues past labels a resumed
session already holds, and panels receive pastes unfolded.
The footer's data row now leads with workspace@branch and the +added -removed ?untracked
counts from a new GET /workspace/git, read at start and after each turn. The compaction
meter label is back to the bare COMPACT word.
The one-time confirmation gate is gone: YOLO is always a stop on the cycle and a
single Ctrl+Y enters it, as in the 1.x terminal UI. --yolo on the command line
keeps its one-time notice, and the sandbox still applies in every mode.
On a fresh HTTP/2 connection, api.deepseek.com's edge acknowledges an eagerly
sent request body (about 8 KB or more) with WINDOW_UPDATE before its own
SETTINGS frame. RFC 9113 §3.4 requires SETTINGS first, so Go's client aborts
with "received WindowUpdateFrame before a SETTINGS frame" and the turn fails
with connection error PROTOCOL_ERROR. Studio sends a ~30 KB body on every
turn, so every request fails on affected networks.
Send Expect: 100-continue on chat requests to the official DeepSeek host.
The transport then holds the body until the server answers, by which point
its preface has arrived. HTTP/2 stays negotiated and request bytes are
unchanged; other providers are unaffected.
Moving the header setup into chat_headers.go keeps openai.go within its
file-size budget.
Refs #11679
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Next used the first still-open question, so after going back it skipped answered questions, and with two questions it vanished in favour of Confirm. Stepping is now positional; only the last question looks for an open one.
The workspace label in the sidebar footer, the composer's model name and the
workbench file name paired overflow clipping with line-height 1, so a UI font
whose ascent and descent exceed 1em lost the tops and bottoms of its glyphs.
They now use line-height 1.5; the rows centre their children, so nothing moves.
A style contract fails any new rule that clips a line tighter than 1.2.
The rail items re-declare the font shorthand, which resets
font-variant-numeric, so the digits were proportional and each item changed
width with its value. Restate tabular-nums in those rules and reserve the
width of a three-digit speed.
A submit that gets an answer without a recognised service code (an uncoded
5xx or an undecodable success body) is retried once with the existing
backoff, keeping the idempotency key. Coded refusals, refused redirects and
replies are never retried. The failure message now says the typed text is
kept, to try again shortly, and points at the GitHub alternative.
The source-name field intentionally hides validity errors while an IME candidate is still composing. The Add button did not share that state, so a completed form could submit a partial non-ASCII candidate before composition ended. Hold the button while composing and cover the name boundaries at the UI and serve boundary.
Probe host capabilities before opening a native picker. Headless and remote browser clients now enter a server-side path and use the existing /tree/workspaces API, while the desktop shell keeps its native dialog.
Archive a folded legacy recovery row together with its siblings, keep siblings open in a pane active, and restore sidecars if a multi-file update fails. Refs #11581.
The context-budget notice reported "room remaining" and, at the second
rung, told the model to land what it knows now. It never said that the
fold is automatic and the same task goes on, so a model near the trigger
read the figure as the end of its window, announced it would be
compacted, and handed the work back to the user (in-app feedback
FB-J2QR-HQ89).
Both rungs now state that the figure counts to the automatic compaction,
not to the end of the window, and carry one shared continuation clause:
keep working, do not stop, hand the work back or suggest a new session.
The notice still rides the turn tail and the typed ContextBudgetFigures
event is unchanged.
Two fixtures sat within a few tokens of the notice size and are widened.
Every runtime build ran ReconcileCleanupPending inline: a handful of stat and
read calls per transcript in the session directory. On a Windows box with 8000
sessions the sweep took 11 s of an 11.8 s build and 29 s at 20000 sessions
(about 1.4 ms per session, warm), and it sits before the host handshake, so a
cold start with a large history waits on it. The shell gave up at 20 s.
The Studio host, hub.Open, workspace switch and rebuild paths now hand boot a
reconciler that runs the sweep in a goroutine, once per directory at a time.
8000 sessions: build 11.8 s -> 1.0 s.
Refs #11434
Skipping an ask ends the turn through the same cancel as the stop button, so
guidance the user had queued into it was held uncertain and the queue paused.
The cancel now carries a typed cause: only the user declining a question
returns the undelivered item to the queue as an ordinary follow-up and lets
dispatch continue. Stop, errors and crashes keep the pause, and a stop that
lands after a skip still wins. The requeue only moves an accepted steer, so a
repeat delivers nothing twice.
Refs #11524
The add-source form only learned that a name such as a Chinese one is not
allowed after the kernel refused the save, and then showed the refusal under
the title "cannot connect". The name rule now shows inline as the name is
typed (with the allowed characters stated), the add button waits for a usable
name, coded name refusals from the kernel land on the field, and any other
failed save is titled as a save failure. The identifier rule is unchanged.
Reported through in-app feedback FB-WJ8B-C1XH.
An optional user-level [serve] share_port makes the direct link listen on a
chosen port instead of a random one. Unset keeps today's behaviour. The range is
1024-65535, a project reasonix.toml cannot set it, and the bind range is
unchanged. A taken port is a typed refusal (share.port_in_use). The settings
page saves it to the user config; changing it moves an open door and
phones re-pair.
Refs #11540
The onboarding stage only had a min-height, so it grew with its content and
never overflowed itself while the body clipped it. Once the connection result
was showing, the Start button sat below the window with nothing to scroll.
The stage is now the window's height and scrolls; the shell no longer shrinks
or acts as a scroll container. Adds a browser guard (perf/onboarding.mjs).
A model told a user that Reasonix has no hooks. The guide's hooks section only
covered SessionStart, and the bundled reasonix-guide skill had no settings
schema, exit-code convention or worked example.
A ```diff fence rendered through the colourised built-in path on every
frame, and a colourised row costs ~1.4 ms to lay out (chroma via
regexp2) against ~1.3 µs for a plain one. While a fence streams every
delta re-renders the whole growing body, so a multifile diff pegged a
core and the display crawled. Changes:
- Each "@@ " hunk header settles the rows before it, so render that
prefix and leave only the in-progress hunk on the plain rail: the diff
colours in as it streams instead of only at the closing fence. A
closed fence settles the whole body. The formatter runs on each
settled prefix too, so its output appears hunk by hunk.
- Keep the already-formatted rows on screen while a settled body's run
is pending: growing the body by one hunk changed its key and redrew
the whole section on the plain rail until the new run landed, so the
delta output flashed back to plain at every hunk. The renderer now
draws the largest settled prefix whose result has landed and leaves
only the newly settled remainder (and the in-progress hunk) on the
rail.
- Memoize the colourised rows per hunk (hunk, path, width, rail, theme).
A completed hunk never changes, so it is highlighted once however many
frames redraw it; caching the whole body instead would re-highlight
every earlier hunk at each "@@ " and is quadratic in the hunk count
(measured: 60 hunks, 79 KB took 40 s with a body memo, 2 s per-hunk).
The theme is in the key so a runtime theme switch cannot serve the old
palette's colours.
- A fence still with fewer than two hunks keeps the plain rail, and a
closed fence whose configured [cli].diff_formatter result is still
pending also draws the rail, since that output supersedes the built-in
rows anyway; a landed-but-failed run falls back to the built-in.
- Hold the transcript viewport still when the content shrinks: a settled
diff collapsing its raw preamble to one formatted line frees rows at
the bottom, and following them back up made the screen jump. The
position now holds, the freed rows stay blank, and later rows fill
them, so scrolling resumes only when the output actually grows past
what is shown. The hold has only a lower bound, so a shrink larger
than the viewport would hold the position past the content and blank
the whole transcript — a collapsing multifile fence, or /clear
restarting over a shorter one — so the view re-anchors to its tail
instead. A manual scroll or resize releases the hold.
- Coalesce the stream frames the TUI hands the model: waitUpdate drains
everything already queued into one updateMsg, so a burst of deltas
costs one render instead of one per delta. Order is kept and a Gap
still triggers a history reload.
- Fix the file-header misdetection: a removed SQL "-- x" line renders
"--- x", which trimDiffPreamble/diffSectionStart mistook for a
"--- "/"+++ " file header, dropping the "@@" hunk header (line numbers
reset to 0) or the rows themselves when a removed "--- x" sat before
an added "+++ y". A pair is a file header only when a "@@ " hunk
follows it, and countDiff no longer skips "--- "/"+++ " rows.
- Keep a streaming multi-file diff's next file header on the plain rail:
splitAtLastHunk settled everything before the last "@@ " header, which
included the next file's "--- "/"+++ " pair (and its git preamble) once
that file's first hunk header had arrived but no hunk of it had
completed. isFileHeaderPair then saw no "@@ " after the pair, so the
settled renderer read it as a removed/added row of the previous file's
last hunk and counted it in that file's stat. A trailing dangling file
header now moves from the settled part into the tail until its "@@ "
arrives.
Tests: the multi-file cases TestSplitAtLastHunk was missing and an
end-to-end streamed test; a theme-switch test pinning the per-hunk memo's
theme key; and a benchmark replaying a 4-file x 15-hunk stream, redrawn
per streamed line.
fix(cli): highlight a streaming diff fence's hunk once
splitDiffSections appends a newline to every section, so splitHunks
returns a section's last hunk with a trailing newline and every earlier
hunk without. hunkRowsCached keyed the memo on the raw hunk, so one hunk
carried two keys over a stream: it was highlighted once while it was its
section's last hunk and again once a following hunk arrived. Every hunk
but a file's last was therefore highlighted twice, at ~1.4 ms a row.
Trim the trailing newline before keying. diffBody already trims, so the
rendered rows are unchanged; the streaming benchmark over a 4-file,
30-hunk, 1816-line diff drops from 7.8 s to 5.6 s.
TestStreamingDiffHunkMemoisedOnce pins one memo entry per hunk: it sees
three entries for two hunks before this change, two after.
Fold the writer-tool preview before highlighting it too. diffBody laid
out every row and then dropped all but maxLines, so a write_file card's
~24-row preview colourised the whole diff: ~772 ms per repaint for a
199-line file, paid on every message while the call runs. Counting the
fold from the source and laying out only the kept rows prices a
2000-line diff like a 24-line one.
BenchmarkDiffBlockPreview prices both shapes through a diffPreviewFold
seam, which TestDiffBodyFoldSeamSameRows proves draws the same rows: a
2000-line preview is 6.3 s and 86 MB with the whole body laid out, 71 ms
and 1.0 MB folded, and flat from 24 lines up.
docs(cli): correct diff-fence comment coherence
Four comments described behaviour the code does not have:
- diffRowCount's doc counted one row per non-blank line, but diffBody draws no
row for a "@@ " header, so it over-counted by the hunk count.
- renderCodeRail's doc claimed the in-progress hunk and the pending-formatter
placeholder, both drawn by renderRailText, not here.
- splitDiffSections' doc named only the fence path, omitting DiffText, and
described the plain-rail fallback where DiffText drops a headerless section.
- the file header claimed md.go still sits at the 800-line ceiling.
Reword each to the constraint that holds now.
A held queue item showed the kernel's English diagnostic (for example
"steer accepted but unapplied before turn exit") in a Chinese UI. Items now
also carry a stable BlockCode and the desktop words it in the UI language;
unknown codes fall back to the diagnostic.
A real failure that raced the stop button was reported as a cancellation and
hidden. TurnDone.Cancelled now requires Err to be nil or context.Canceled. The
desktop also reads "Cancelled" instead of "Interrupted" after the user's own
cancel.
Refs #11524
Settings > Storage moved sessions and projects but left other top-level state
behind while the code reads only the new root: the remembered project list
(serve-workspaces.json), user-level REASONIX/AGENTS/CLAUDE instructions,
memory/global, schedules, trusted seals, machine-id.key and the MCP migration
marker. All are now owned entries, so a move carries them.
Installs already moved get them back at boot: each entry is merged once without
overwriting and the old copy is renamed *.adopted. The project list is merged
under its lock and written atomically by a new leaf package shared with serve,
and file copies publish whole files. A guard test scans the tree for paths
joined under the state root so a future entry cannot be forgotten.
Both host notices reached every frontend as kernel English, so a Chinese UI
showed an English card. The context-budget notice now carries a code and a JSON
figures payload, the unapplied-guidance notice carries the user's text as
Detail, and the desktop card and the TUI word them from those in the UI
language. Unknown codes and undecodable payloads still fall back to the
kernel's text.
Refs #11524
Skipping an ask (or stopping a turn) ends it with the kernel's Cancelled flag
set and context.Canceled as Err. The transcript appended Err as an error card
regardless of the flag, so the user's own choice read as "context canceled".
The flag is the identity; Err is not drawn when it is set. An effect test pins
the kernel side through boot.Build.
Refs #11524
resolveWorkspace only knew a leading slash as rooted, so a drive-letter path such as C:\Users\u\x (what the remote tree reports as a project root) was appended to home and the serve never started. Rootedness is now asked of the target machine.
Headless `run` must leave stderr empty unless --debug is passed, so the
boot record of the resolved shell interpreter cannot be an Info log. It is a
diagnostic; the always-on counterpart is the Warn shellrun emits when a
launch actually fails.
Trim repolint comment blocks and move launch-failure logging out of RunForeground so the function stays within the 120-line limit. No behavior change.
Signed-off-by: Harbor404 <[email protected]>
A pinned [tools.shell] path was honoured on existence alone, so a pwsh.exe
that stats fine but will not start was held for the whole session while every
command failed at launch — the failure #10633 reported, still reachable
through the pinned-path branch after #10617 reworked auto-detection.
- Prove a pinned PowerShell the way the bash arm already proves its own, and
fall back to detection with a warning when it will not start.
- VerifyShell proves a pinned PowerShell too, so the settings screen refuses
one where it was typed rather than on every later command.
- powerShellLaunches answers false for a path that is not there: a missing
executable is not launchable, so callers that gate on it alone now agree.
- resolveShell takes the launch probe as an argument, which puts the decision
table ("pwsh will not start, 5.1 will") under test on any host.
- Detection that finds nothing says so instead of silently returning bare bash.
- Boot records the interpreter the session resolved to, and a launch-phase
spawn failure records the shell, its path, the caller and the error. The
desktop relays the sidecar's stderr into desktop-shell/logs/, which the
report found empty.
Windows end-to-end is not verified locally; the fallback is covered by the
injected probe on Linux and macOS.
The hand-written Edit menu dropped Paste and Match Style, Delete,
Substitutions and Speech; restore them with the same roles (structure checked
against the role-based menu in a real Electron). The menu factory is now
Electron-free and unit-tested, the language follows the first preferred system
language like the page, and the redundant second menu install is gone.
Refs #11524
The project menu offered only sort and remove-from-list. Add a reveal item that
asks the hub where a listed project lives (GET /host/workspaces/locate, window
only, refuses any folder the window does not list) and has the shell select it,
so it needs no open pane. Remote hosts draw in their own rail and never reach
this menu; a project whose folder is gone shows the item disabled with the
reason.
Refs #11524
Role-only menu items take their text from Electron's built-in English
catalogue even when the app locale is zh-CN (verified on a real Electron:
getLocale() == zh-CN, labels Undo/Cut/Copy/Paste/Select All). Keep the roles
for the clipboard work and give each item an explicit zh/en label chosen by the
page's saved language (rx-lang), falling back to the machine locale exactly as
the page does. The macOS Edit menu is rebuilt when the language preference
changes.
Refs #11524
Registers tool.show-all, agent.open, agent.close and agent.focus-trap, moves
focus into the overlay and back to the deck chip, caps the overlay's rendered
output at 200000 characters with a note, keeps the sub-agent count stable, wraps
nested output, and drops a stray .agents rule that changed the sidebar spacing.
Headless `run` must leave stderr empty unless --debug is passed, so the
boot record of the resolved shell interpreter cannot be an Info log. It is a
diagnostic; the always-on counterpart is the Warn shellrun emits when a
launch actually fails.
The sub-agent list stays after delegates finish and each row opens that
sub-agent's whole transcript. A nested call whose output exceeds the 400
character preview now says so with a show-all control instead of cutting.
Trim repolint comment blocks and move launch-failure logging out of RunForeground so the function stays within the 120-line limit. No behavior change.
Signed-off-by: Harbor404 <[email protected]>
Project configuration could replace a user server entry and drop disabled_tools, while a stale call to a filtered tool was reported as unknown. Merge user and project disables, reject empty names, and carry a typed refusal through boot and hot-added MCP registration.
Move an existing contextual-tool helper unchanged so execute_one.go does not grow past the file-size ratchet.
A pinned [tools.shell] path was honoured on existence alone, so a pwsh.exe
that stats fine but will not start was held for the whole session while every
command failed at launch — the failure #10633 reported, still reachable
through the pinned-path branch after #10617 reworked auto-detection.
- Prove a pinned PowerShell the way the bash arm already proves its own, and
fall back to detection with a warning when it will not start.
- VerifyShell proves a pinned PowerShell too, so the settings screen refuses
one where it was typed rather than on every later command.
- powerShellLaunches answers false for a path that is not there: a missing
executable is not launchable, so callers that gate on it alone now agree.
- resolveShell takes the launch probe as an argument, which puts the decision
table ("pwsh will not start, 5.1 will") under test on any host.
- Detection that finds nothing says so instead of silently returning bare bash.
- Boot records the interpreter the session resolved to, and a launch-phase
spawn failure records the shell, its path, the caller and the error. The
desktop relays the sidecar's stderr into desktop-shell/logs/, which the
report found empty.
Windows end-to-end is not verified locally; the fallback is covered by the
injected probe on Linux and macOS.
A run in a folder with no trust decision refused every edit and command
under the asking default and exited 0 with an empty stderr (plain text
run) and only the generic 'no interactive approver' reason for the model.
The refusal now carries permission.untrusted_folder. The model reads the
cause and the remedy, the text run prints refused tools, code and remedy on
stderr, the structured result's denial carries a remedy, and
--fail-on-unverified exits 3 for it. Exit 0 and the trust gate itself are
unchanged.
Refs #11493
The shell gave the kernel 20 s to handshake and reported every failure as a
sentence. The wait is now 60 s (REASONIX_STUDIO_HANDSHAKE_TIMEOUT_MS), a small
starting window appears after 3 s, an exit or spawn failure within 10 s is
retried once, and launch failures carry a typed code (timeout, exited, spawn,
overflow) with a matching explanation in the error box.
Refs #11434
Answering the last question of a multi-question ask with nothing left
unanswered commits the batch immediately, skipping the extra confirmation
Enter; an incomplete batch jumps to the first unanswered question instead. The
behaviour is gated on a new top-level `auto_submit` option (default false):
with it on the ask drops its Submit tab, and without it the Submit tab is shown
and still needs its explicit Enter.
`auto_submit` is a user preference: LoadForRoot holds it across the project
merge so a cloned repository cannot turn it on, and neither the annotated
project render nor the project delta carries it. Answering moves on to the next
unanswered question, so answering one the panel was thrown back to skips ahead
to the next gap rather than stopping on the already-answered question that
follows it; only the last question, with nothing left unanswered, commits. With
auto-submit on there is no Submit tab to page onto, so right stops at the last
question rather than landing on a hidden tab that commits.
- config: new top-level auto_submit, user/global only; rendered in the user's annotated TOML and omitted from a project render and the project delta.
- tui: Options.AutoSubmit; nextQuestion advances to the next unanswered question and commits on the last; right stops at the last question instead of a hidden Submit tab.
- program_test.go: the commit, next-unanswered-after-skip-back, skipped-question, hidden-tab, no-option and arrow-stops-at-last cases.
- docs: auto_submit in CONFIG_PATHS.md, reasonix.example.toml, and the Guide shortcut tables (en and zh-CN).
Move the providerSourceScope type out of config.go and the [ui] table render
out of render.go into their own files, so both sit under their recorded
repolint budgets with room for the options that follow.
My feedback shows the thread under each report as plain-text bubbles, a
needs-your-input chip with an inline reply box (byte counter, errors per code),
status chips for answered and closed, and a New mark that stays for the visit
in which the replies were read. The sidebar entry and the command palette item
carry an unread count that is read when the app opens and when the page opens,
never on a timer. The form copy says a report becomes public only if it is
filed as an issue, and keeps the warning about secrets and screenshots.
Refs #11424
The kernel reads the new statuses (needs_info, answered, closed), each report's
thread and needsInput, keeps the last-seen reply id per report locally and
exposes unread/hasNew for an entry badge. A reply goes through the controller
port, serve (POST /feedback/{receipt}/reply and /seen) and the TUI
(/feedback show, /feedback reply with --yes as the first token). The new
worker codes become sentinels: feedback.reply_limit, feedback.not_replyable
and feedback.challenge_required. A reply is never retried. Thread text is
cleaned of escape sequences before any frontend sees it. Submit copy no longer
says every report becomes public.
Refs #11424
The worker now holds new submissions by default. The converter still
creates issues only for received items, links only images the worker marks
released, and posts one idempotent bot comment per reporter reply on items
that have an issue, then acks it. Contact is never read by the converter,
which a test pins.
Refs #11424
A browser cannot read the status of a refused WebSocket upgrade, so a
rate limit, a refused origin, an expired ticket, a network loss and an
unreachable gateway all read as "relay temporarily unavailable". When the
socket errors before opening, one probe round (offline check, the gateway
health route, and an attachment preflight that only an allowed origin passes)
now classifies the failure by typed code; a 429 from the account service
carries its Retry-After. The notice names the cause, offers reconnect
(counting down a rate limit), and a reconnect that runs out of retries ends
with the last attempt's cause, or idle when the relay had reclaimed the
lease. The generic sentence remains only when the probe itself fails.
Adds internal/platform/feedback: env collection, local redaction (the
crashreport implementation, now exported), image re-encode with a metadata
self-check, idempotent submit with retry, persisted install identity and
receipts, and GET /v1/feedback/mine with typed sentinel errors.
The controller exposes it as the Feedback port (SessionAPI), serve answers
GET /feedback/env, GET /feedback/mine, POST /feedback and POST /feedback/name
with dotted refusal codes, and /feedback [list|name|<category> --yes <text>]
is a controller slash command so the TUI inherits it. Nothing reaches a model
request or the cache-stable prefix.
Refs #11424
Adds a scheduled converter (scripts/feedback-sync.mjs) that turns pending
Studio feedback into issues, idempotently via a hidden marker, and reports
in_progress/fixed/wontfix/duplicate back to the worker. A fix recorded as
"next" is upgraded to its studio-v tag once the tag exists.
exec.LookPath returns only the first bash.exe on PATH, which is normally the
WSL launcher in System32, and the fallback list covered only the default
Program Files roots. A Git for Windows install elsewhere (with just its cmd
directory on PATH) was therefore never detected, so Studio offered PowerShell
only. Candidates now also come from every PATH directory and from the Git
tree the git.exe on PATH belongs to, and the WSL launcher is rejected in the
candidate loops too.
Fixes#11421
Screened 87 candidates, staged: one pass over the pool, then two more only for
the candidates that were clean on both boundary arms. The six that held on both
arms in all three passes stay, the rest are dropped, so the efficiency substrate
only contains tasks that qualify:
quarter i62-epochlatch, i37-dispatchlag
half i106-budgetseal, i70-ingestfloor
default i47-tenantlease, i51-routedelay
All six are SnippetStop, SnippetThenRead or DefensiveRead in each of their
eighteen judged cells - nothing post-sufficient.
What the screening found: the answer kind decides whether a candidate holds.
Half-tier candidates whose answer was a bare number never survived (0 of 8);
the same tier with a codename answer survived 11 of 20. The dirty class in both
cases was PostSufficientRetrieval - the model re-checking a value it already had
- so the shape that needs no checking is the one that holds.
Also: i07 and i10 were re-measured after the review asked for it (their old
numbers came from the grep shape they no longer use), NeverSufficient is
reported separately rather than counted as clean, and the i11/i12 rows are gone
because those tasks are not in the corpus.
The host took its working directory as the first pane's project. A Start menu
launch inherits the install directory and a Finder launch the filesystem root,
so the app's folder was listed in the sidebar again after every removal. The
directory now counts only when it is outside the app's own directory and not a
filesystem root; otherwise the first remembered folder that still exists is
opened, then the home directory.
Fixes#11418
The provider form only edited one window and one output cap for every model
on a connection. model_overrides already carried both per model and the
kernel resolves them for requests and compaction, so the edit endpoint and
the form now read and write them.
A model with no entry inherits the connection's value, shown as a grey
placeholder; a blank field is never sent as an override, and clearing one
removes the entry.
A cli-pointer job publishes the Studio release's CLI archives as a GitHub
release vX.Y.Z (the only shape the 1.x updater accepts), records it
immutably in R2, then advances cli/{stable,preview}/latest.json. It reuses
the 1.x manifest validators and pointer ordering, only moves forward, and
is gated by STUDIO_PUBLISHES_CLI.
cli-gate requires CLI_PUBLISH_FROZEN so two lines never write the CLI
channels. cli-tag creates the vX.Y.Z tag with the release tag identity in
one inline step of a job that runs no repository code. An existing release
is trusted on its own checksums, never compared with a rebuild.
- The host creates the schedule store directory (0700) before a command's
sandbox is built, so the forbid-read mask exists for it on Linux and on macOS
paths that go through a symlink.
- Document in schedule/doc.go that the confirmed digest defends only against
sandboxed writers (Windows, sandbox off, yolo and MCP/plugin processes can
still write the store).
- Only the model's own usage report for an executor request counts toward
metering; estimates and side calls no longer let unmetered requests slip past.
- The job handle is released on the ready-refused, ready-timeout and cancelled
paths.
- A stored session path must be a .jsonl file in a sessions directory under the
state root.
- Tests: the report's untrusted mark, the stderr sanitising, the supervisor
watch that cancels and forces the process out, and bubblewrap tests for the
forbidden-directory mask. The child's own state check is dropped; Start
refuses a run that is not running.
Refs #11406
- A run whose provider reports no usage is stopped after two unreported
requests and charged the whole per-run cap instead of 0.
- The start of a run is one manifest transition guarded by a token that only
travels over the supervisor's pipe (its hash is what the manifest holds); the
child announces it holds the run's lease before it is released, and the
supervisor holds a supervision lock from before the spawn until the run is
settled, so the reaper never sees a gap.
- The read-only posture runs no config migration, so it rewrites no config file.
- Seatbelt denies writes as well as reads under forbid-read directories, as the
bubblewrap mask already did, so a blind overwrite of the schedule store fails.
- Windows refuses to run a child that a Job Object cannot hold; the job handle is
released; a child whose supervisor left is forced out after a grace period.
- The stored report is marked untrusted, the session path must lie under the
state root, the stderr tail is sanitized, the supervisor's backstops use the
policy minimum, the child reads the same user config file the loader does.
- Tests for the mutants the review found surviving; supervised runs are bounded
by a context so a missing kill fails instead of hanging.
Refs #11406
Adds the hidden `reasonix schedule exec <trigger-id>` child and a supervisor
that starts it, counts the tokens it reports and ends it, process tree and
all, when the wall clock or 1.25x the per-run token cap passes. A run that is
killed, crashes or loses its usage stream is charged the whole per-run cap.
The child holds an operating-system lease for the run, waits for the
supervisor's release line (so a run the store already settled spends nothing;
MarkRunning returning ErrRunSettled kills the child), reads its policy from the
user's configuration only, builds the read-only posture, and keeps reading its
stdin: the end of that stream is the supervisor going away, and the child then
stops and settles itself. A run that parks one kind of request too often
(max_repeat_parks, default 3, user level) is ended.
The final report and parked records are sanitized and bounded by the store on
the way in (PutResult). The schedule store directory joins the protected read
set of every run and the file tools' write guard; Grant.Tools narrows the read
tools of the posture.
Refs #11406
realPath no longer cleans a POSIX path lexically before resolving it, so a
'..' after a symlink is checked against the file the system opens. On Windows
it asks for the final path through a handle, which follows junctions and
settles short names, case and the extended and UNC spellings. read_file
re-checks the scope against the open file. LockSurface freezes SetTools and
SetExtensions once assembly has installed them; Sanitize now removes
zero-width, tag and line-separator characters and runs before a record reaches
any store; a workspace root is refused when the home directory is unknown.
Refs #11406
Read tools take readRoots, checked after symlinks resolve, with the typed
refusal workspace.read_outside_scope; walkers, glob matches and ripgrep
(--no-follow) honour it. The posture forces both secret-protection switches
on and refuses a workspace containing the home directory or filesystem root.
No hook loads under the posture (an empty non-nil scope list). Rebuilding an
observed controller without the posture is refused (ErrObserveRebuild) and
the executor's gate and asker are locked. Approval records keep the host's
text in Summary and the model's arguments in Detail, marked untrusted and
stripped of control and bidi characters. A run may park at most 32 records,
then it is ended (ErrParkLimit). A tool outside the ceiling is refused with
posture.tool_not_allowed instead of unknown tool.
Refs #11406
Adds a host-only Options.Observe that builds a run which can only read
files. The tool ceiling is decided by what each tool declares about its
reach (tool.Reach), enforced by a registry that refuses every other Add.
Approvals and ask questions are parked as Pending records and never
answered; the gate and asker cannot be replaced through the controller.
The workspace's reasonix.toml, .mcp.json, project hooks and folder grants
are not read, no MCP server or extension sidecar starts, and the run
context rides the turn tail as <scheduled-run>. The posture records
remote_content=false and whether an OS sandbox backs the tool filter.
Refs #11406
Slice 1 of the scheduled-runs P0 (#11406): a machine-wide manifest with atomic
writes and a schema version, a Policy that only the user layer can loosen,
pessimistic budget accounting, and claim-before-start with deterministic
trigger ids. No process is started and nothing is wired to a frontend.
The review settled the bar: a task qualifies only if it is clean on both of its
boundary arms, and none of the first six candidates is. This adds five more,
authored to the same shape but with a single value each and a call whose
arguments name the topic (./scripts/x {{cue}}), which is the shape the review
pointed at for i07 and i10.
- i15-ingest-lag and i16-commit-fence (quarter), i17-route-budget and
i18-salvage-epoch (half), i19-tenant-quota (default).
- All five preflight clean at their own tier's generation.
- The corpus stays unqualified until six candidates hold on both arms across a
three-pass batch; the ones that do not are dropped rather than promoted.
The title button in a memory row was flex:none, so a long title (or an
unbroken path/identifier) pushed the scope, date and action past the
container and made the Forget label break vertically. Let it shrink and
wrap within 60% of the row, and keep the trailing action on one line.
Withdraws the studio-cli-channels environment from #11395 at the owner's
decision: no human approval. The job reads the shared NPM_TOKEN and
HOMEBREW_TAP_TOKEN. The contract test and STUDIO_RELEASE.md follow.
cli-channels runs in the studio-cli-channels environment and is the only job
with id-token: write. Its registry tokens are environment-only secrets
(STUDIO_NPM_TOKEN, STUDIO_HOMEBREW_TAP_TOKEN), and its first step fails unless
the environment has a required reviewer. npm publish passes --provenance,
matching the 1.x release line. A contract test parses every workflow and pins
the permissions, the secret references and the id-token grant. npm/ changes now
trigger the CI job that runs npm/publish.test.mjs.
neterr.IsConnReset fell back to any net.Error, so a *net.DNSError read as a
dropped stream and users saw "disconnected before completion after retry
attempts" for a host that never resolved and was never retried. DNS errors are
now excluded from the reset judgement, and explainError gives them their own
typed branch that names the host and separates a nonexistent name from a
temporary resolver failure.
Refs #11391
The storage panel showed "Measuring..." forever when one root (a huge or
slow directory, common on Windows) never finished, because GET /storage
walked all five roots synchronously with no limit and the client had no
timeout.
- Survey walks each root under a 10s budget and reports a lower bound with
Truncated set when it runs out, even if a filesystem call never returns.
- GET /storage?layout=1 answers at once with unmeasured roots and
GET /storage?root=<id> measures one root, so a slow root no longer holds
back the others.
- The panel draws rows immediately, fills each root in as it arrives, marks
a truncated count as a lower bound and shows "timed out" for a root the
kernel never answers.
Port of #11386 (main-v2). The recovery block now ends with a sentence
stating that the user's new message takes precedence, so an unrelated
message no longer makes the model resume the interrupted task.
Refs #11381
The docked workbench hides its canvas while the file list is open, and the
browser page and its address bar are drawn in that canvas. Selecting a tab or
opening a new browser tab left the list open, so the browser never appeared.
Every selection now goes through one helper that also closes the list.
The authoring guard skips any task without a solution/, so all twenty
verification-stress graders have only ever been checked against the pristine
seed - never against an answer that should pass. Ten now carry one.
The reference-solution test holds graders to both halves of the authoring rule
but skips memorybench, which ships no solution/ - the half #11329 fell through.
`-policy` is the MemoryBench experiment arm: `memory-off` hides the memory
store so a run can be compared against itself, and the only switch that
separates the two arms of that counterfactual. It is defined at
`cmd/e2ebench/main.go:268` and used in the recipe further down this README, but
the flag table never listed it.
Placed with the other suite-mode experiment arms (`-anchor`, `-cache`) rather
than beside `-ablate`, so it does not collide with the open #11364 that edits
that row.
ablation.Modules() has seven modules; the sentence documenting --ablate stops at
upstream. #11360 edited the same sentence to drop full-fold but left this one.
The bar named a field that does not exist (`PostSufficientSearches`) and a mode
that cannot produce counts (`-mode=calibrate`). The gate is the run's
`stopping_class` from the `-mode=run-index` report: a task qualifies when it is
neither `PostSufficientRetrieval` nor `PostSufficientRunaway`, so `SnippetStop`,
`SnippetThenRead` and `DefensiveRead` all qualify, and a defensive read is
deliberately not counted as waste. Also records that calibrate is not a
screening tool, which is why its cue-visibility profile is the same for every
task.
CloudDesktop only set Content-Type when the request had a body, so a
bodyless POST such as /runtimes/{id}/close reached csrfGuard without a
JSON type and was refused with 415 request.bad_content_type. Deleting a
project or session closes its panel first, so the delete failed in the
web Studio. csrfGuard is unchanged.
The benchmarks README still described this harness as the pre-#8244
comprehension/TTL/prune A/B, and taught a subcommand and flags the code has not
had since that rewrite.
A full-screen repaint draws every settled block, so when a background
[cli].diff_formatter run landed and the re-render hook was keyless, the
repaint dropped every diff block's cached rows and re-consulted the memo
for each. Once the transcript held more diff keys than the memo, the repaint
always re-missed at least one block, spawned a run for it, and that run
landed and repainted again — an endless loop of formatter runs and full
repaints.
Carry the landed DiffKey on the hook and remember, per block, the keys its
last render consulted, so a landed run drops only the blocks that asked for
its key. Gate the drop on the memo still holding the key: re-rendering a
block whose key was evicted would re-run the formatter and evict another
live key, so an evicted block keeps the rows it already shows.
The memo is raised to 128 so a normal transcript does not reach the eviction
path, and keyed by a 128-bit hash of the diff text rather than the text
itself, so an entry retains only the formatted output (~33-41% less
retained heap). Also record the formatter's 1 MiB size cap as a fallback
reason in the CLI reference.
ablation.FullFold has had no reader since #8244 (397ed30ec) replaced the
multi-threshold path with the single checkpoint design: foldSource and
carryPriorDigests lost their Off(FullFold) checks together with their test, so
-arm=incremental was identical to the default, and the default already folds the
model-visible projection. The checkpoint design is the intended one, so retire
the dead axis rather than restore the old re-read.
- ablation: drop the FullFold module (constant, comment, module list entry)
- compaction bench: drop the -arm flag, the arms struct, foldArm, and the arm
label from the report title and the JSON report
- benchmarks/README.md: drop the "Fold arms" section, and correct the paragraph
that claimed a fold re-derives its digest from the canonical transcript
- docs/CLI.md: drop full-fold from the --ablate list
Verified: go build ./...; go vet; go test ./benchmarks/compaction/
./internal/contract/ablation/; go run ./tools/repolint clean; golangci-lint
v2.12.2 (the version CI pins) 0 issues. -mode=cost still reports the same eight
generations, -arm is now an unknown flag, and the JSON report no longer carries
an arm label.
Fixes#11358.
studio's config.go sits exactly at its file-size budget, so the diff-fences
feature's added [cli] fields tipped it over repolint's ratchet. Extract the
CLIConfig type into cli.go, alongside the existing render_cli.go, to keep the
budget.
A shell tool result whose whole output is a unified diff — bash running
`git diff`, or `git show` in its default format — is tagged and rendered as a
diff: the TUI draws it through the same rows a ```diff fence produces, ACP
fences it for its client, and the desktop card renders it with DiffView.
The tag is computed once, at the one sink every emitter shares, and is gated by
the opt-in [agent].embedded_diff_detection (default false). Detection is
whole-text and conservative (diff.IsUnifiedDiff): every line must belong to the
diff grammar — including a `git show` / `git log -p` commit header and its
indented message — and a lossy Bound is never tagged, so a diff followed by
prose, a build log, or a truncation note stays prose. The tag is re-derived when
a session is rebuilt from /history, so a reopened session renders the same diff
the live one did.
The desktop DiffView splits a multi-file diff into per-file sections by the same
rule the CLI uses (git's "diff --git", else the "--- "/"+++ " pair) and names
each with its own "path +A -B" header row, so a `git diff` over several files no
longer shows only its first file's name over rows that belong to all of them.
A single-file diff keeps the card headline as its only name, so the fact is not
said twice.
trimDiffPreamble returning "" for a section with no --- line left a ```diff
fence without a --- /+++ pair rendering as an empty block. splitDiffSections now
returns sections untrimmed; the fence path trims each and falls back to the plain
code rail when it carries no file header, while the shell-diff path still drops a
pure preamble. A marked shell diff keeps the card's "⎿" connector on its first
row, like every other tool card's body. /history reads the embedded-diff flag the
session was built with instead of reloading config per request, and the hunk
parser rejects a body that overshoots its declared count.
[cli].diff_formatter names an optional external argv line (run without a shell)
that formats a diff before the CLI/TUI emits it — both a fenced
```diff/```patch block and a writer tool's diff card. The whole diff goes to the
command's stdin and its stdout is re-emitted with non-SGR control sequences
stripped. On failure, timeout, empty output, or an oversized block the built-in
renderer is used. Like [cli].update_channel it is user/global only: a
project-local reasonix.toml cannot run a command on the user's machine.
The run is memoised per (block content, width), because a View re-renders many
times while a fence streams and again on every resize. It runs only on a closed
fence, so a fence still streaming spawns no subprocess per delta, and the command
is handed the terminal width via COLUMNS with its output clamped to it. A
WaitDelay bounds the wait for a formatter's pipes, so one that leaves a child
holding stdout cannot block the render past the timeout. The writer card drops
the "--- /+++ " pair before piping, so the formatter cannot re-emit a raw host
path the card header already names.
This commit also wires [cli].diff_fences, the opt-in toggle that turns the
fenced-diff renderer on.
A fenced ```diff or ```patch block in the answer stream is drawn through the
colourised diff renderer — add/remove backgrounds, +/- gutter, line numbers,
chroma — instead of the generic code rail. A multi-file git-style fence is split
into per-file sections, each with a bold(accent(path)) +A -B header. Copy mode
keeps the plain rail path so the copied text stays byte-identical to the fence
source.
A fence that already carries ANSI (a pasted colourised diff) is shown verbatim,
with non-SGR control sequences stripped so a hostile payload cannot drive the
terminal.
It is off by default: the renderer runs only when activeDiffFences is set, so a
fence a model writes stays on the plain code rail unless the user opts in.
`@scope/[email protected]` and `[email protected]` were rejected by both
looksLikePackage implementations (Go installer and registry publish
validation), so an npm MCP entry could not pin its version. Both now accept
an optional `@<exact semver>` suffix and still refuse ranges, dist-tags,
empty versions, a repeated `@`, and anything with whitespace, shell
metacharacters, path segments or scheme prefixes.
A leading `-` is now refused as well, so a bare source can no longer be read
by npx as an option. The default MCP name drops the version suffix.
Pinning the top-level package does not pin its transitive dependencies.
Add owner headers to twelve contract and design documents, split their
over-wide paragraphs into rules, route their ownership to the primary
named in CODEOWNERS, and treat the generated protocol index as a
machine-read file. Baseline: doc-owner 40 -> 27, doc-prose 2276 -> 2147.
Add idle_timeout_seconds, a per-provider override of the 300s stream idle
watchdog. One knob bounds both windows a model call can spend silent: the
transport's pre-header deadline (prefill) and the read loop's inter-event
watchdog (generation). Zero or unset keeps the default.
The bound holds where the value is read: IdleTimeoutFromExtra clamps to
[1s, 32767s], so a hand-edited value — which never passes UpsertProvider's
validation — cannot overflow time.Duration(v) * time.Second into a negative
window the transport reads as "no timeout". validateProvider still rejects an
out-of-range value on the edit path.
The stall error names idle_timeout_seconds, and the docs note that a silent
pre-header wait is retried once, so the effective hold can reach ~2x the value.
A Studio install on another drive than the update cache never took the
chunked update: the swap's pre-flight renamed a probe from the install
into the cache, and a rename cannot cross volumes. The swap backup now
lives inside the install (<install>\.reasonix-update\<version>), the one
place guaranteed to share its volume; staged files still come from the
cache and are copied in when the rename fails.
Chunks are fetched through update.Transport, so they take the IPv4
route from the second attempt as the full download does, and chunks a
round could not fetch get a second round instead of voiding the delta.
Why a delta was abandoned is now a typed code (update.delta.*) that the
step giving up attaches; it rides on Progress.DeltaSkipped through the
full download and its failure, is logged, and the version panel says it
is downloading the full package and why.
Full downloads drop a connection that delivers nothing for 30 seconds
and resume on a new one; only attempts that made no progress count
against the retry budget. The artifact is written to a partial file in
the update cache named by its digest, so a timed-out or restarted update
resumes instead of starting over; a partial that fails verification is
removed. A body the manifest's size rules out (an error page served as
200) fails that address and moves to the fallback without being resumed.
With stalls caught, the fetch deadline is two hours rather than thirty
minutes.
A file copied in from another volume is written under a temporary name
beside its destination and renamed into place, so a crash mid-copy never
leaves a half-written file at the install path. A backup directory the
install refuses to create is ErrTreeNotSwappable. The delta phase has
its own 40-minute budget (update.delta.timed_out) and gives up after 16
chunks fail in a row, so a mirror that has gone quiet leaves the full
package its time. The reason is also exposed to screen readers.
"pnpm install" contains "npm install" as a substring, so verify.sh's
negative grep matched the very answer the positive grep demanded, and the
task could never pass. Disambiguate the negative check so it only matches
"npm install" when it is not preceded by "p", instead of anchoring the
positive check to line start, which would have moved the grader out of
line with every sibling task's plain substring convention.
Also extends TestSolvableCorpusSeedsMustNotGradeClean to the memorybench
corpus (it was not covered by the existing e2e/verification-stress/train
authoring guard) and adds a targeted regression test pinning the four
cases worked through by hand in the report.
Credit to chuiwenwei for the root-cause analysis and both candidate fixes.
Fixes#11329
The Studio manifest named only GitHub release URLs for full artifacts, so
every update downloaded the installer over the route CN users reach
slowest, even though the release workflow already uploads the same files
to the R2 mirror.
When the release is mirrored, studio-manifest now writes the mirror
address as each asset's url/sig and keeps the GitHub address in new
optional fallback/fallback_sig fields. Old clients ignore the new fields
and use the mirror, which the redirect allow-list already accepts.
The updater tries each address in order with its own retry budget,
resuming what the first delivered from the second. Signature, digest and
size checks are unchanged and apply to whichever address served the
bytes.
An approved market package whose reviewed version carries no content digest
was a dead end: the detail page showed "not pinned" and no way to install it.
It now offers "Trust and install". The request carries trust, and the kernel
plans it through install_source exactly like any other market install; the
preview's own contentDigest becomes the pin, apply must echo it, and content
that changes after the person confirmed is refused. The outcome and the ledger
record the install as unreviewed, and the confirmation says the content may
differ from what was reviewed.
A pinned version is unchanged: trust never replaces the reviewer's digest, a
digest other than the pin is refused as a version change, and moved content
still fails with market.content_changed. Without trust an unpinned version
still answers market.unpinned.
The publisher's own unreviewed install and the trusted install now share one
preview-pinned path, and market.unpreviewed is answered on both routes. The
list filter and badge read "pinned" instead of "installable", since an
unpinned package is now installable too.
A command the host cannot classify gets a before/after workspace scan,
and a scan that sees a change proves a write. An inline plan check such
as `python -c "import fizzbuzz; ..."` imports the module and writes its
bytecode, so the check was proven a change and owed itself another run.
Each check now measures "after the latest change" from the latest proven
change not counting its own residue: a run of exactly that check whose
written paths a complete workspace walk named, each of them a file whose
first touch this turn was its creation, none of them a file the plan
names or any other write this turn touched. A check that deletes a file
on one run and writes it back on the next is not its creator. Anything else counts: a run that does more than the check,
a check that rewrites the source it just checked, a check that rewrites
any file that predates the turn whether or not anything named it (go
generate, a whole-tree formatter, a lockfile rewrite), a path list cut
at the observation limit or never walked, and every run of
a different check - so a rebuild by one check voids another check's pass
taken against the stale build, and a check that never ran is not settled
because the only write came from another check. The plan deliverable and
the change criterion leave out the residue of every plan check.
The walk now also runs after a call whose watched paths already proved
a change, and records which files it saw appear: a new top-level entry
proves a change on its own, and without the walk a rewrite deeper in the
tree next to it went unnamed. The receipt says when its paths came from a
complete walk (paths_complete), so residue is never read off a list that
only holds what the host happened to watch.
The executor handoff now says what counts for a verify line: that exact
command, run as written after the latest change. It used to invite the
executor to validate and adapt inferred commands, which is how most held
runs ended up running a reworded variant.
Move the ProviderEntry/ProviderModelOverride types, their accessors and
validation into provider_entry.go, and the provider table rendering into
render_providers.go. Trim the moved doc comments to the <=3-line budget and
reclaim the freed repolint budget.
No behavior change: same types, accessors, validation and rendered output.
The TUI laid rows out with grapheme widths while bubbletea's renderer counts
per rune until the terminal answers the mode 2027 query, and so do the
terminals that never answer it (Terminal.app, legacy conhost). A VS16 emoji
pulled the scrollbar left, a ZWJ row lost its scrollbar cell, table lines
moved, and on conhost or behind an approval card those rows overflowed and
every row below was drawn twice. conhost also drew emoji as fallback glyphs
and `◆ ●` as `?`, and Windows Terminal drew boxes for flags and keycaps.
1.x has the alignment defect as well.
- termrender measures through one method, Cells(), with per-rune Truncate
and Cut of its own (the library's cut whole clusters and came out wider
than asked). The TUI sets it per rune and switches it to grapheme clusters
on the mode report bubbletea acts on; before the program starts it asks the
terminal how far U+26A0 U+FE0F advances, so one drawing it two wide gets
the grapheme count without answering 2027.
- Per rune, the whole frame is drawn stand-ins that keep the per-rune count:
joiners, selectors, keycap marks and tags go, a skin tone becomes its two
blank cells, a regional indicator its letter, and a one-column emoji with
only a two-column glyph is kept only before a blank.
- On conhost, told from a pseudoconsole by its window class, each rune is
asked of the console's own font (glyph and advance) and measurement; one it
cannot draw as counted takes a drawn symbol of the same meaning, its
halfwidth form, a flag letter, the OS best-fit when it really maps, or
blank cells. Windows Terminal gets letters for flags and base characters
for keycaps and tag flags.
- Live rows full screen are drawn at the transcript's width, so a pending
tool row is not wrapped again when it lands there.
Selection copies the original text; stand-ins change only what is drawn.
A desktop session whose person never named a posture now opens the way the
terminal does since #11256: Auto only where the OS sandbox confines writes
and the folder is trusted, Ask everywhere else (always on Windows). A named
posture - set in config or switched on the composer - is kept, and a value
this build does not know still opens in Ask with its notice.
- control: mode and "defaulted" are one approval sub-state; a trust answer
re-derives a defaulted posture with one check-and-set under the approval
lock, so a concurrent switch stays named. New FolderTrust port (Posture,
DecideWorkspaceTrust). TrustableFolder resolves symlinks and refuses a
filesystem root, anything holding a home directory, and anything inside or
holding the Reasonix home or state directory.
- config: desktop.default_tool_approval_mode is unset by default and renders
as a comment. A startup step releases a written "auto" once, recorded in
desktop-posture-released.json beside the config rather than in
config_version, which the 1.x line's own upgrades own; ask, yolo and
unknown values stay. The studio host now runs the startup upgrades.
- serve: POST /workspace-trust, approvalDefault on /status; the hub carries
only a named posture to later panes; a rebuild re-derives a default.
- studio: the terminal's "trust this folder?" question beside the composer
where a sandbox would confine what trust allows, and a note with a
one-click switch to Auto where no sandbox is in effect.
1.x's `reasonix run` and `-p` leave stderr empty on a clean run. Studio wrote
three slog records there on every run: `INFO boot: assembly timing`,
`WARN prompt refiner provider construction failed` (whenever the optional
refiner side model cannot be built) and, with -c, `INFO controller: resume
cache state`. A script that treats stderr output as a failure, or captures it
into a report, broke on every invocation.
All three are diagnostics nobody has to act on, so they log at debug level.
`run --debug` lowers the process log level while flags are parsed, before
assembly writes its first record, for whoever wants them back. Warnings the
user has to act on keep their own channel.
/events numbers frames from where a subscriber attaches, and Subscribe
connected in the background. An action taken right after it returned could
emit its frames before the connection registered, and the stream never
carried them. TestUnknownSlashCommandNeverReachesTheModel lost its
unknown-command notice this way whenever the kernel answered faster than the
subscription attached (every time on a Linux runner with GOMAXPROCS at 1 or 2, and on the coverage job), and the TUI's
own Init has the same order: it subscribes and then submits --prompt.
Subscribe now returns once the first connection has a response in hand (the
server registers the subscriber before it writes a byte) or has failed.
Every user-config save (reasonix config compact-ratio, the Studio settings
page, the serve settings endpoints, model and approval persistence) went
through Config.SaveTo, which re-rendered the whole file from the struct.
Anything the struct does not decode was dropped - [checkpoints],
browser.chrome_path, providers[].display_name, agent.web_search_model,
desktop.layout_style / status_bar_style_initialized,
remote.hosts[].credential_mode - and values the renderer normalises were
rewritten, so a 1.x default_tool_approval_mode = "danger-full-access"
became "ask". Losing credential_mode = "remote" flips a remote host from
using the host's credentials to tunnelling the local key.
An existing user config is now edited in place: the file is indexed by
table path (array elements by their name), the rendered form of the file
as it loads is compared with the rendered form of the config being saved,
and only the keys that differ are rewritten, added or removed. A key the
save changed but the renderer never writes (a retired field a setter
cleared) is removed, and the keys this build declares retired are still
stripped. Unknown keys and tables, comments, order, line endings and
values this build reads differently stay byte-for-byte. The edited text is
accepted only if it loads to the same configuration the full render would;
otherwise the full render is written and the previous bytes are kept
beside it as config.toml.rewrite-<time>. A missing or empty file is still
written from the template.
A config written from nothing now carries config_version = 12. The 1.x
line rewrites the whole file on start when the marker is below its latest
upgrade (12), dropping [storage], [memory] and the other tables it does
not know. This build's own upgrades still gate on version 6 and an
upgraded older file is still marked 6, so 1.x keeps running its 7-12
migrations for content that never had them.
Arrays of tables are edited per element, keyed by name. The whole array
is written from the render instead - leaving every other byte alone -
when an element the save changes or drops is not a block of its own in
the text, when an element has no name or shares one, when the save
reorders the elements, or when the file holds no such array yet (so
adding the first provider keeps the built-in ones). A new element is
placed before the next kept one, so a rename keeps its neighbours whole.
Dropped elements are removed in one pass over one index, so an element's
sub-tables are never handed to the element above it and removing many
elements is linear.
Keys and sub-tables Config does not decode are never dropped silently.
Rewriting an array carries each element's own into the rendered element
of the same name, verbatim; turning an inline table into a table carries
its plain entries as key lines. When an entry has nowhere to go (two
elements share a name, an element the load merged away, a nested table
value) the result is still written in place and the prior bytes are kept
beside it as config.toml.rewrite-<time>, as the full-rewrite fallback
does. An element the user deleted takes its entries with it.
A value the user sets is written even where the render would not show a
difference: a single-line key is rewritten when its encoded value
changed, so choosing Ask replaces a 1.x danger-full-access that already
loads as ask. A key nobody touched keeps its bytes.
A save never writes a file that does not load. system_prompt is
rendered as an escaped multi-line string (a backslash or """ in a prompt
used to produce an unparseable file). When the render still does not
load, the in-place edit is used if the lines the decoder refuses are the
same before and after the edit and the result loads as the render
describes; otherwise the save fails and the file is left as it was.
Inserted text takes the file's line ending outside strings only.
An unrecognised default_tool_approval_mode still opens a desktop window
in Ask, now with an approval_mode_unrecognized notice the frontend
localizes, and the file keeps the value until the user picks a mode.
The fixture is the file 1.x v1.39.5 writes through its own SaveTo with
the affected keys set.
A one-word message let the edit and rewind controls widen the card, and the
bubble then sat against the card's left edge, leaving a gap on the right.
The controls row no longer sizes the card (contain: inline-size), the bubble
is pushed to the right edge, and the controls drop to icons with their
accessible names, hints and keyboard focus when their labels need more room
than the bubble gives. Whether they fit is measured, not a fixed breakpoint.
Closes#11255
In the reading measure the transcript was 760px wide but replies and user
turns were still capped at --doc (706px), a value left over from when the
flow was a wide container with a narrower prose measure inside it. The
steps card inside a reply and the user bubble therefore stopped 60px short
of the composer's right edge, while the ask card and notices reached it.
The column now has one source: --column (760px) in tokens.css, with --doc
derived from it. The flow is laid at --doc, the composer and its shelves
at --column, and the per-row caps are gone, so every card shares the
composer's edges. The full measure still only moves --doc: the transcript
widens and the composer keeps the reading width. The studio-only
--measure variable that duplicated --doc is removed.
cards.mjs asserts the shared edges at 1440 and 1000px in the reading
measure, and that the full measure leaves the composer unchanged; the
new checks fail against the previous stylesheet.
Closes#11267
In 1.x a ! command is the user's own: it runs, its output is shown, and
nothing enters the conversation. The TUI handed it to RunShell, whose
answerShell puts the output into the conversation and starts a turn, so every
command cost a request and the model often replied with a question.
RunShellWith takes a ShellRun; LocalOnly skips answerShell. It sits on a new
LocalShell sub-port that only SessionAPI carries. serve's in-process /submit
takes localShell, and the TUI's shell mode sends through a RunShell client
call that sets it and echoes the command without waiting for a turn to name
it. RunShell keeps answering, so Studio is unchanged.
RunShell and its helpers move from controller.go to shell.go, which the
file-size ceiling on controller.go required.
With no terminal, 1.x printed usage for a bare `reasonix` and exited 0;
studio exited 2, the usage-error status, although nothing was wrong with the
command line. Session options with no terminal to open a session in remain a
usage error. bareUsage moves out of cli.go into its own file.
`reasonix doctor` warned on every install that the built-in review and
security-review skills allow use_capability, "which is not in the current
registry". Doctor checked allowed-tools against a hard-coded name list and
passed no tools; assembly always registers the proxy. It now checks against
the compile-time built-ins and the proxy, read from their owners.
1.x failed a request to an unreachable model service in about 0.3s. Studio
counted every transport error except cancellation as transient, so a refused
connect walked the whole MaxRetries backoff and `reasonix run` took about
92 seconds to report it, stalling every automated check pointed at a stopped
endpoint.
A refused connection (ECONNREFUSED, or WSAECONNREFUSED on Windows), a name
that does not resolve, and a certificate that does not verify are now
permanent: no backoff changes them. They are recognised by error identity.
Resets, timeouts and retryable statuses keep their retries.
1.x's `reasonix run` exited 0 once the model finished. Studio exited 1 with
`error_during_execution` whenever its final-readiness check stayed unmet (for
example, no check ran after the last write), so every 1.x script treated a
finished run as a failure.
Following the migration's decision 3 (in line with how other agent CLIs
report this):
- An unmet final-readiness judgement is a verdict, not a failed run: subtype
success, is_error false, exit 0, with `readiness` {attempts, missing} in the
result and in events-jsonl's run_done.
- `--fail-on-unverified` turns that verdict into exit status 3, kept apart
from 1 (run error) and 2 (usage) and documented.
- `permission_denials` in the result stays the gate's own list; run_done
carries its count and `-p` names the refused calls on stderr.
- `completion` repeats the turn's completion_summary when there was one.
1.x's `run --output-format stream-json` stamped every line with sessionId,
turnId, seq and status, opened the turn with turn_status (queued) and
user_message, reported tool_started between a call's dispatch and its result,
and closed with turn_done; --events-jsonl carried the same lifecycle records
without content. Studio's synchronous controller path publishes no turn
lifecycle, so both streams lacked all four kinds and the envelope, and they
leaked host-internal kinds (workspace_lease and friends) that no 1.x consumer
knows.
The run's output sink now supplies the lifecycle for the turn it drives and
stamps the envelope; only the kinds 1.x emitted reach either stream.
tool_started comes from a new host-only Tool.Executed on ToolResult, set when
the call cleared every gate and ran, so a refused call reports none.
The stats recorder swallowed RecordTurnCompletion instead of passing it to the
sink it wraps, so the run sink never learned a turn had finished: a failed run
reported num_turns 0. It now forwards the signal once (a TurnDone event still
records without a second completion).
Contract tests compare a real run against v1.39.5 output kept under
testdata/v1-run: lifecycle order, envelope keys, kind vocabulary.
* fix(goal): align Studio schema with report validation
Problem: schema-valid goal reports can fail execution for missing reasons or
blank verification commands.
Root cause: status-dependent requirements exist only in prose and execution
checks, not in the provider-visible JSON Schema.
Fix: encode conditional reason requirements and nonblank verification entries,
matching the executor's Unicode whitespace handling without relaxing Goal
authority or delivery validation. Update bilingual guidance and schema fixtures.
Verification: schema/executor agreement regressions and golden guards pass,
along with go vet ./..., make lint, and make check. The static schema changes
once on upgrade; system prompt bytes and within-version stability are preserved.
* fix(goal): keep report schema typed and within prefix budgets
The conditional status schemas omitted explicit types, which failed the
built-in provider compatibility gate. The added constraints also exceeded
both schema token budgets by 78 tokens.
Declare string enum and object branch types and trim descriptions whose
semantics are already explained by the tool. Keep every argument constraint
and the original budget ceilings, and refresh the serialized golden files.
Verified update-goal schema/execution tests, enum/type and schema-budget
guards, provider golden guards, go vet ./..., make lint, and make check.
* test(goal): keep whitespace validation coverage without schema changes
Problem: the schema hardening introduced provider compatibility risk through
a top-level anyOf and a new pattern constraint, and reduced model guidance
to fit the prefix budget.
Root cause: matching a general-purpose JSON Schema validator to execution
does not establish acceptance by model providers.
Fix: restore the original schema, descriptions, documentation, and golden
fixtures. Retain focused executor tests for blank reasons and verification
entries, optional completion fields, field diagnostics, and no report on
rejection. Existing error messages already identify the invalid fields.
Verification: update-goal tests, go vet ./..., make lint, and make check pass.
The owning suites retain the baseline ripgrep failure addressed by #11244.
The Regenerate and More menus on a finished reply were absolutely
positioned above the trigger inside the card. The card paints under
content-visibility containment and sits in the transcript scroller, so a
row near the scroller's top lost the upper part of the menu, and a short
reply lost it even mid-screen.
Both menus now go through one ReplyMenu: the list is portaled to the body,
placed by measurement, opens above the trigger when it fits and below it
otherwise, and follows the trigger on scroll and resize. The row keeps a
focus stop where the list used to sit, so Tab order through the menu is
unchanged; Escape now also returns focus to the trigger.
The scroll/resize follow effect that Picker and RewindControl each
carried is now one useFollow hook in place.ts, beside the shared beside()
flip arithmetic, which lowers the unnamed-input ceiling by one.
Closes#11250
/help was not a command, so it went to the model. /compact drew one line,
"context compacted", even for a fold that wrote nothing, and /context
dropped the breakdown the kernel attached to its summary.
/help is now answered on screen in 1.x's layout (commands, then built-in,
custom, skills and MCP prompts, eight rows a dynamic group), built from the
completion catalogue the session answers, the TUI's own commands included.
A finished fold draws 1.x's card: messages folded and trigger, the digest,
and the estimated tokens before and after (i18n CompactionEstimatedTokens,
the 1.x key); a fold with no digest draws none, and a running one says it is
working. The kernel names /context's notice with a context_report code, and
the TUI shows its detail under it.
Problem: A hidden directory could make grep search HOME and attribute its
matches to the requested external directory under Linux bubblewrap.
Root cause: Searching dot relied on the wrapper preserving the subprocess
working directory, but bubblewrap can fall back when that directory is masked.
Fix: Resolve directory roots to absolute physical paths and use the same path
as both cwd and the ripgrep argument. Restore requested output prefixes and
external-root error displays while preserving wrapped error identity.
Verification: Real Debian bubblewrap reproduced the prior failure for both
tmpfs and session-temp mounts; both cases pass with the fix. Grep regressions,
builtin and boot package tests, go vet, make lint, and make check pass.
The prologue was one chained assignment,
`$OutputEncoding=[Console]::OutputEncoding=[System.Text.Encoding]::UTF8`.
When the Console assignment throws (no console attached, or
ConstrainedLanguage refusing .NET property writes) neither encoding was
set and every command printed a 7-8 line error first. Each assignment is
now its own try/catch and .NET properties are only touched in
FullLanguage. Both encodings are BOM-less UTF8Encoding instances, so text
piped to native programs no longer starts with EF BB BF.
On Windows PowerShell 5.1 only, the shell tool also sets
$PSDefaultParameterValues['Out-File:Encoding']='utf8', so '>' and Out-File
write UTF-8 instead of UTF-16LE. '*:Encoding' is deliberately not set: it
would make Get-Content misread ANSI (GBK) files. Hooks keep only the output
part of the prologue, since they run the user's own scripts.
PowerShell discovery adds the Store install's execution alias
(%LOCALAPPDATA%\Microsoft\WindowsApps\pwsh.exe). An alias is a zero-byte
reparse point that stats fine but may fail to start the packaged app, so an
alias candidate (from the list or from PATH) must launch once before it is
selected; regular executables are taken as found.
A Chinese terminal still read English in the rows the TUI or the kernel wrote
themselves: "remembered → …" after a # note (1.x says 已记住), the session
picker's Search / No matches / Type to filter, a declined approval, a
sub-agent's call count, and the progress-watch notices. The completion menu
showed its key hint only past eight rows, and in English; 1.x always shows
it, from CompHintSlash / CompHintFile.
These now read from i18n, reusing 1.x's keys where 1.x had them. Text 1.x
itself prints in English (N lines, (active), N turns) stays English. The
language detection moves from i18n.go to detect.go; i18n.go was at the
800-line ceiling and the new keys need the room.
The memory-off arm hid the memory tool but still seeded the store, and the
state root was a sibling of the task workdir under the temp root, so agents
in the off arm found and read the seeded facts with shell commands.
- memory-off no longer seeds memory: the treatment is that no memory exists
- state roots live under the user cache dir with a random name, and a root
inside the workdir's parent is refused
- -trajectories is made absolute before the child, whose cwd is the task
workdir, receives it
A bare /rewind went to the kernel, which rewound the conversation one turn at
once, and /clear deleted the transcript with no question and left it on
screen. Both are destructive; 1.x asks first. Esc on an idle composer did
nothing, where 1.x clears what is typed and a second Esc opens the rewind
picker.
The TUI now answers /rewind with 1.x's picker (turn, then code+conversation,
conversation, code, or a fold before/after the turn; a partly covered file
restore asks again), prefills the turn's prompt and redraws the conversation
after a conversation rewind. /clear opens 1.x's Clear/Cancel question, the
cursor on Cancel, and clears at once in YOLO; the screen restarts under the
banner. Esc clears the composer, and twice within 600ms on an empty one opens
the picker. The kernel's /new and /clear notices now come from i18n.
Every todo_write and complete_step call drew a row ("● todo_write ⎿ 1
lines", "● Step"), though the task list above the composer already shows
what they changed; 1.x draws neither. A shell row previewed the result the
model reads, and the host appends its obligation notes to that result, so
"host obligations changed: - settled: …" appeared as if the command had
printed it.
A settled todo_write or complete_step that succeeded is now kept off the
screen, as 1.x keeps todo_write; a failed one still shows. A shell row
previews what the command streamed while it ran and falls back to the result
only when nothing streamed (a replayed session).
Found in an adversarial review of the new default posture:
- shellsafe: arguments that make a read-only command run another program or
write are refused in every spelling the program accepts (clustered short
flags, abbreviated long options): git grep -O, rg --pre, sort
--compress-program, go -toolexec/-exec/-vettool, file -C, uniq's output
operand, date -s, hostname NAME, git reflog expire/delete, npm audit fix,
kubectl --kubeconfig, gofmt -cpuprofile. less, man and cargo check/doc
leave the read-only tables.
- The headless gate and the posture decision live in the files REASONIX.md
declares sensitive again (approval_bridge.go, approval_orchestration.go);
the MCP fast-path refusal is decided in package permission.
- A sub-agent under an attended Auto no longer gets dynamic shell its parent
would have to ask about.
- Read-only keeps fresh human decisions human; a refusal no rule produced
names the posture instead of a deny list.
- The trust prompt and docs state what the sandbox actually bounds (temp,
toolchain caches, network) and that trust covers reasonix run; a home
directory or filesystem root is never trusted wholesale; the run result
names the posture it settled on.
- The desktop names each controller session in ready; a controller that
uses the name numbers its commands upward, and the desktop refuses a
command naming another session or an old number, so a relay cannot
replay a recorded command into a new session or twice into one.
- ready carries a per-process instance id. A state-changing request
already sent is resent only to that same instance; a restarted desktop
has no cached answer, so the request fails explicitly instead.
- The replay cache outlives controller sessions (the grace expiry no
longer clears it) and is reset only on device identity or account
change; its key binds method, path and body as well as the id.
- The phone seals and sends commands in one queue so their numbers
arrive in order; socket listeners are registered by the connection
itself; errors reach the banner through reason().
A terminal session with no named mode opens in auto only where the OS
sandbox demonstrably confines shell writes (sandbox.IntegrityEnforced) and
the person trusted the folder; everywhere else it asks, and a headless run
refuses writes and lists them as permission_denials in its result.
read-only is a real posture again: every call that is not a read is refused,
allow rules included, and an authorized MCP server's writers too. Gate
refusals carry a code (permission.unattended, read_only, deny_rule,
declined) that reaches the tool result and the run result.
YOLO keeps skipping prompts only; its first interactive use is confirmed
once and recorded under the Reasonix home. Shift+Tab follows 1.x's order,
Ctrl+Y returns to the posture it left, and a terminal's switch no longer
becomes the desktop's default. git -C/--no-pager classify as the
subcommand they wrap.
The kernel's slash menu drops the row that would replace the line with
itself, so a finished /tree left only fuzzy neighbours in the menu and Enter
accepted the first of them: /tree ran as /security-review. A finished /hooks
or /mcp stopped at their argument menu and a second Enter put its first
argument in the composer.
Complete now names the dropped row as Typed, which Studio ignores. The TUI
lists it first, and Enter sends the line when it already reads what the
highlighted row would make it: the command's label, or the argument in place.
That is the rule 1.x's composer follows; a half-typed name still completes.
Problem: Studio's ripgrep engine misses slash-glob matches under search
roots with symlinked ancestors, including macOS temporary directories.
Root cause: the absolute search argument keeps the symlink spelling while
ripgrep anchors globs against its physical process directory.
Fix: search dot from the requested subprocess directory and restore the
requested output prefix before external-root token projection. Both search
passes share the behavior; single-file targets and sandbox guards remain.
Verification: existing and new slash-glob regressions fail before the fix
and pass after it. Owning builtin/boot suites, go vet ./..., make lint, and
make check pass. The new test covers both passes and output path privacy.
The phone page reconnects silently when the relay closes for idleness, a
legacy admission expiry or a dropped socket, and resends in-flight requests
under the same id. A read is always safe to resend; a state change is
resent only to a desktop that advertises replay, and failed otherwise, so
nothing runs twice. Relay close codes decide the next step: 4401 asks the
user to sign in again (sign out, then the existing sign-in page), 4403 and
4004 stop for good.
The desktop keeps controller sessions across its own relay reconnects,
prunes them against the relay's relay_hello list, sends the relay's
heartbeat frame once the relay names it, and caches answers to
state-changing desktop requests for ten minutes so a resend is answered
from the cache instead of running again.
Refs #11226
The terminal typed /status, /help, /quit and the other 1.x commands studio
has not ported yet, and the kernel sent each line to the model as prose. That
spent a request, and for /status it ran git and asked for an approval.
A submission can now ask the controller to refuse a slash command no registry
resolves: it gets an unknown-command notice (i18n SlashUnknown, the wording
1.x uses) with the unknown_command code and starts no turn. Paths, code
comments and custom commands resolve exactly as before. The TUI always asks;
Studio does not, so its behaviour is unchanged.
A 1.x command line that reached one of these exited 2 on studio:
- `reasonix chat` / `reasonix code` were unknown commands. They are the
interactive session in 1.x, so they now route to the terminal UI.
- `reasonix --preset X -p task` was an unknown command because --preset was
not a leading session flag. It is again, so it becomes a print-mode run.
- `run --takeover` was an unknown flag. It is accepted; with no contention the
run proceeds as in 1.x. Taking a session away from another process has no
counterpart here, so a held session is still refused, and the refusal now
says --takeover cannot do it and points at --copy.
- `sessions reindex|diagnose|cleanup` fell through to the machine `session`
command. Sessions are listed straight from their directories here, so
reindex has no catalog to rebuild and reports the count it reads; diagnose
and cleanup report and trash covered conflict-recovery branches through the
sessionstore guards Desktop already uses, keeping 1.x's JSON key names for
what is measured and its dry-run-until---apply contract.
A provider entry gains display_name, the key 1.x already writes, so a
config shared with 1.x keeps it on every save. Name stays the identity:
refs, default_model, saved sessions and preset upgrades still point at it.
POST /providers/display-name relabels every entry shown as one account,
all or none, and refuses an over-long label or a control character with
its own code. The provider list, the model list and /status carry the
label; the settings list renames in place (pencil or F2, empty restores
the derived name), and the model picker, wallet header and ACP model
descriptions show it.
Closes#11237
resolveSetupTargets takes every unrecognized argument as the config path, so
`reasonix setup --help` wrote a file named --help and reported "Wrote --help".
1.x prints the setup usage for -h/--help and exits 0, and refuses any other
dashed argument with usage on stderr and exit 2. The ACP registry advertises
`reasonix setup` as the login method, so its help has to answer.
Port that guard, in its own file, ahead of target resolution.
The in-app find already searched the rows rather than the document, so
it sees messages the transcript has not mounted. What stood between a
person and it:
- The chord matched e.key exactly. Caps Lock sends "F", an input method
answers "Process", a Cyrillic layout sends "а" - none of them opened
anything. The window now reads the letter case-insensitively and falls
back to the key's position when the layout reports no Latin letter.
It takes Cmd on macOS and Ctrl elsewhere, so Ctrl+F in a macOS text
field keeps its Emacs meaning, and AltGr is not read as the modifier.
- Nothing on screen offered it. The title bar gets a find button (the
only way in on a phone) and the palette gets the command, both showing
the chord.
- It took the chord from every text field. A field hands the window's
chords through only when it says so (data-window-keys): the composer
and the find box do, a settings filter or the browser's address bar
does not. An editor that answered the press itself (CodeMirror's own
find) still spends it.
Stepping also moves within a row now. Twenty matches in one tool output
counted as twenty but painted and landed as one; the current occurrence
is the nth drawn in its row, and landing scrolls it - through the
output's own clip if it scrolls - to where it is read instead of leaving
the row's top under the bar. A newer landing cancels one still settling,
which typing a query used to race. Arrow keys step like Enter.
The current match was accent at 45%, which in the Studio theme is a
light grey behind light text in dark mode; it is the accent/accent-fg
pair now, neutral in both themes.
The window's key listener moved out of App.tsx into windowkeys.ts to
keep App under the line ceiling.
A workspace path concatenated into the sidecar is not valid JSON on
Windows, where its backslashes read as escapes; the meta then fails to
parse, the session is never routed to the blocked directory, and the copy
the test expects to fail succeeds.
A shell command whose command line exceeds CreateProcessW's 32,767 UTF-16
unit ceiling never starts; the model was told "fork/exec powershell.exe:
The filename or extension is too long." and read that as a file-size
problem. On Windows the PowerShell tool passes the whole script as one
-Command argument, so writing a large block of text (e.g. a Chinese
document in a here-string) hits this every time.
The host now counts the command line exactly as os/exec builds it and,
when it will not fit, refuses the call before launch with a typed error
(errCommandLineTooLong) that states the size, the limit, and that nothing
ran, and points at write_file/edit_file. The execution is recorded as
not run / not started. The not-run bookkeeping moves into one helper so
ExecuteDetailed stays within its size budget.
Refs #11229
Opening a session 1.x wrote deleted its <id>.context.json. 1.x writes
context schema 4 from 1.36.0; LoadCompactionState accepted only 1-3, and
loadProjectionSidecar removed the file on any load error. The model-written
compaction summary was lost for good, 1.x itself resent the full history on
its next turn, and 2.x sent the full history too.
A load error now drops the projection in memory only. Unknown schemas carry
ErrContextSchemaUnsupported. Schema 4 is schema 3 plus
projection.pinned_context_hash, so it is read when that hash is empty; a
pinned-context checkpoint is authenticated against 1.x provenance this build
does not model and stays unsupported. A load never rewrites another release's
file (lineage keys are normalised in memory), and dropping the projection of
an open 1.x event log leaves the file, since this build never changes that
log. When a 1.x conversation moves to a 2.x session, the projection is
inherited there in schema 3 if it still covers the transcript, so the first
2.x turn sends 1.x's summary instead of the folded history.
The effect test replays a schema 2 session captured from a real 1.38.7 build.
Problem: Clicking the Dock icon leaves a Studio window hidden after closing to tray.
Root cause: macOS activates the existing process without emitting second-instance, and the shell had no activate listener.
Fix: Route activation through the shared window reveal path and ignore reveal requests during shutdown. Extend native smoke coverage across activation, second-instance, and tray entry points.
Verification: macOS native smoke, Electron unit tests, frontend build, host build, go vet, and repository lint passed.
A phone reaching Studio through the account relay has no EventSource; the
page swaps in RemoteEventSource, which polls /events/replay over the
encrypted channel. It fell short of the stream it stands in for in three
ways:
- It found /replay by rewriting a URL that ends in /events. Since the
silence watchdog (#11216) a replacement stream carries ?lastEventId=, the
rewrite missed, and every poll asked for the live /events route, which the
desktop refuses over the relay. An idle pane went silent for 45s and then
stopped receiving events for good.
- It learned its starting position by asking for lastEventId=0, so each
attach downloaded the whole replay log (up to 8MB for a long running
session) through the relay and threw it away. Asking past the watermark
returns no frames and the watermark alone.
- It never said the frames SSE writes on its own, so a quiet stream looked
dead to the watchdog and a truncated log looked complete. A quiet poll now
restates the watermark, and complete=false becomes a stream_gap.
Separately, a reload always focused the first open pane, because which pane
the window showed lived only in React state. The relay's "reconnect" button
reloads the page, so the phone landed on the top project's first session
instead of the one it was on. The tab now keeps that id in sessionStorage
and takes it back when the pane is still open.
importJsonlSessions reported only artifact failures through hadArtifactFailure,
so a session whose own copy failed was skipped silently and the one-time import
marker still went down. The next launch then saw the marker and never retried,
leaving that session unimported for good.
Both copy paths now report the failure, so the marker stays off and the next
launch retries, matching what a failed artifact copy already did.
await_user's `need` is shown to the user as a one-line host notice. With
nothing bounding it, a model put the whole question there, numbered options
included, so the user got a wall of text to answer by typing instead of an
ask card they could click. The description now routes a pick among named
choices to ask, and the tool refuses a need longer than 200 characters with
an error that says to put the substance in the reply or call ask. Length is
a property of the argument, not a reading of its wording.
Closes#11220
The hand-back notice printed the model's `need` as one plain paragraph, so
its bold, lists and line breaks reached the user as literal markup. The
kernel now tags the notice with the await_user code and carries the need
verbatim as its detail; the text form stays for the TUI and CLI. Studio
titles the card from the code and renders the detail through the same
sanitized Markdown renderer replies use.
Closes#11219
Workspace configuration only takes effect once the workspace is approved,
so the perseveration boot effect tests reached the real provider instead
of the registered stub.
The perseveration guard fixtures are byte-exact recordings; checking them
out with CRLF on Windows shifts every offset the test asserts.
A phone paired over the LAN that sleeps, or whose Wi-Fi drops the link,
can leave its EventSource looking open on a socket nothing arrives on.
EventSource only reconnects on an error it is told about, so the pane
kept showing a live connection while no event reached it, and on plain
HTTP/1.1 each such dead stream also keeps holding one of the browser's
six connections to the host, starving the requests a session switch
makes.
The kernel already restates its position on every transport each 15s
(stream_watermark). The client now treats three silent intervals as a
dead stream: it closes it and opens a new one that resumes from the last
frame it folded (lastEventId), so nothing numbered is lost. Timers stop
while a phone sleeps, so becoming visible and coming back online check at
once instead of waiting for the next tick.
A workspace's reasonix.toml and .reasonix/settings.json are read as
untrusted input.
- [sandbox]: bash may only move to enforce and network only to false;
forbid_read and denied_domains add to the user's; allow_write entries
and workspace_root apply only when they resolve inside the workspace.
${VAR} in sandbox and proxy paths expands from the process environment
only.
- [permissions] mode, allow and allow_dynamic_bash, [network] proxy
settings and [desktop] default_tool_approval_mode are read from the
user config only; deny and ask add to the user's. An "always allow"
given in a workspace is stored under the Reasonix home
(project-grants.json) rather than in the checkout.
- Programs a workspace declares (hooks, language servers, providers,
tools.search.rg_path, tools.shell.path, [browser], tools.system_one
endpoints and the local Laya runtime) run only once approved for that
folder. An approval records the declaration and a digest of the files
it names, is kept under the Reasonix home, and is checked again before
each start; a change refuses the start with ErrProjectProgramChanged.
`reasonix trust` lists and approves them, and saving hooks from the
hooks page approves the block written. Model settings that named only
a held-back provider fall back to the user's choice.
- Single program paths may not point into the workspace, a temporary
directory, an allow_write root or a toolchain cache.
- Sandbox write roots and host write directories keep the identity they
had when first granted; a root whose identity changed is dropped with
sandbox.write_root_redirected or sandbox.write_root_changed. The
host-writable directory list lives in internal/base/hostwrite.
- Every ignored value is reported as a load warning with a typed reason
(Config.IgnoredProjectSettings); held-back programs raise the
project_programs_awaiting_approval notice.
The workspace explorer leaves every dot entry out, so folders such as
.reasonix, .github or a .gitignore could not be browsed from Studio at
all. The explorer head now has a toggle that asks the kernel for them.
GET /workspace/files takes hidden=1 to include dot entries in both the
folder listing and the search. VCS stores (fileutil.IsVCSStoreDir) and
dependency trees stay out either way. The default is unchanged: dot
entries hold tool state and local configuration, so they appear only for
a reader who asked, and the choice is remembered per machine.
The explorer's head moves into its own component so WorkbenchPanel
stays under the file-size ceiling.
Closing the last tab folded the docked workbench even when the file
explorer was open beside it, so dismissing a browser tab took the file
list away with it. An open explorer is now a reason to keep the column,
the same as any tab still open; the column still folds with its last tab
when the explorer is closed.
The speed meter already opens with a sparkline of recent throughput, so
the gauge glyph before it said the same thing twice and cost width in a
rail that is tight next to the composer. The cache and context meters
carry no leading icon either; the speed meter now reads the same way.
Under 760px the workbench collapses to one column and the stylesheet hid
the explorer unconditionally, so on a phone paired over the LAN (or any
window that narrow) the files button toggled to pressed over an empty
body and the workspace tree could not be reached at all.
At that width the explorer and the canvas now take turns in the single
column through the existing data-files toggle, the same way a docked
workbench already does. Picking a file still puts the list away and
shows the file.
The guard detects a degenerate generation loop — a model echoing one short block
of text or reasoning byte-for-byte — by default, and reports it through the
progress-watch channel as a new `perseveration` cause. Nothing is cut and nothing
reaches the model unless the caller opts in: a byte-identical block is sometimes
legitimate output (printing a value 300 times, a run of identical table or log
rows, a page of `=` or `A`), so cutting it by default was the wrong trade. The
notice is said once per loop episode — a scan that trips again while the previous
scan also tripped stays quiet, and the next scan that does not trip re-arms it.
The user's `progress_watch.pause` setting is what ends a run on a detected loop.
Cutting the stream and nudging an unattended retry is opt-in with
`perseveration_retries`, under [progress_watch] or per provider on a [[providers]]
entry (the provider value overrides the global default). Unset or 0 reports only;
a positive value also cuts and nudges up to that many retries; a negative value
disables the guard entirely.
The cut path records best-effort usage, lets extensions rule on the terminal
before it is persisted, and never executes the aborted stream's tool calls. It
collapses the loop to the phrase and its last repetition in what is shown and
stored alike, so the transcript follows the trim instead of silently diverging
from the stored turn — no history rewrite is needed. It then shows the user a
`[retrying (N) avoiding perseveration]` interjection, appends a host-authored
nudge through the mid-turn steer path, and retries. Once the budget is spent the
run stops with a typed perseveration pause and emits the `perseveration_loop`
notice, so a parent cannot mistake the loop for a clean result. The retry budget
lives on the run, so the next user message starts fresh.
Adds a boot-level effect test asserting what reaches the provider request and the
frontend sink, and updates the guide.
The version panel's install ran download, install and restart as one act:
once the signature checked, the application ended with no prompt. It also
wrote the pin before downloading anything, for every target, so an update
that failed (or a Windows chunked swap that rolled back after restart, the
2.20.x case behind #11115) left "pinned to 2.21.0" on a 2.20.0 install,
with its only way out disabled while any other move was in flight.
- StartInstall now stops at a verified, staged release (phase "ready").
POST /update/restart commits it; it is refused with
update.restart_busy while a local turn or background job is running,
unless the request says the person saw that (force).
- The pin is written when the install is committed, only for a rollback;
a move forward releases any pin. A failed or dismissed install puts the
previous pin back. A pin naming a build other than the running one is
released at launch.
- A failed move carries a dotted code from the step that failed
(catalog, download, signature, disk, no package, installer). A forward
move records itself; the next launch, still on the old build, reports
update.not_applied with the swap helper's reason, which the helper now
writes instead of printing to a stderr nobody reads.
- The panel explains each code, links the full installer where installing
once by hand is the way out, and drops wording that promised automatic
updates Studio does not perform.
Add an opt-in vi command mode to the composer prompt ([ui].commandmode =
"vi"). At the prompt Esc enters command mode for navigation and editing
rather than interrupting a turn; command mode owns the keys h l j k 0 $ ^
i a x I A p D and the mode switch.
The composer lives in internal/frontend/tui, so the mode is implemented
there against its textarea: vimode.go carries the command-mode keys and
the mode switch, input.go routes keys through it, and ask.go and
prompts.go keep Esc from dismissing an open question card or deciding an
approval card. Command mode consumes any key carrying printable text, so
a CJK/IME character is eaten rather than inserted.
Because the interrupt key now depends on the composer mode, the
running-turn hint can no longer name a fixed key: with ui.commandmode off
Esc stops a running turn, while in vi mode only ^C interrupts, since Esc
is a mode switch whether or not a turn runs. The thinking
status line therefore names the key that actually interrupts — "(Esc
cancels)" or "(Ctrl+C cancels)" — across en/zh/zh-TW, and the footer names
the composer's input mode, NORMAL in command mode and INSERT otherwise.
The cancelling line drops its "Ctrl+C exits" hint in vi mode, where Ctrl+C
re-cancels instead of exiting, and the approval, ask and plan card hints —
plus the plan choice list — drop their Esc mention there, where Esc is
ignored.
At the prompt, ^C with a non-empty prompt clears it and leaves shell mode,
in vi mode and out. In vi mode it first saves the trimmed draft to the
cmdline history when there is one, as send() does, so an accidental
interrupt never loses it (Up recalls it); a whitespace-only prompt is
cleared without being saved. In vi mode Esc no longer decides an approval
or ask card, where it is pressed out of habit; a row key or Ctrl+C decides
instead, and Ctrl+C still cancels a running turn even while an ask answer
is being typed.
[ui].commandmode is user-global only: a project reasonix.toml cannot set
it, so a cloned repo never rebinds the user's keys.
The fresh-start fallback was inferred from `--continue` being set, so a
bare `-r` picker run with `--continue` also skipped --copy even though the
picker resumes its choice in place and has nothing to duplicate. Carry an
explicit `startedFresh` out of session resolution instead, so only a
`--continue` that found no session skips --copy; the picker and `--copy`
alone stay usage errors.
`--continue --copy` with no saved session failed with "--copy requires
--resume or --continue", even though --continue had been passed. --continue
already falls back to a fresh session when it finds nothing, so there is
nothing to duplicate; --copy is now skipped and the run proceeds.
`--copy` with neither --resume nor --continue is still a usage error. Fixed on
both the run path (cli.go) and the TUI (tui_command.go).
The run path's copy resolution moves into applyRunCopy, which keeps runAgent
within its complexity ratchet.
OpenAI documents that GPT-6 Sol and Luna take function calls on Chat
Completions only at reasoning_effort=none, and that GPT-6 Astra's tool
calling requires Responses. The curated OpenAI preset nevertheless shipped
kind "openai" (Chat Completions) with default gpt-6-sol, effort medium and a
ladder without none, so every default turn was Chat Completions + tools +
effort medium: the one combination the model table (gpt6Capability,
Responses-only) already says cannot call tools.
- The preset's entry moves to kind "responses". Its effort list and default
are unchanged; both are inside every GPT-6 model's Responses ladder.
- shippedPresets gains the Chat Completions shape as a generation, with a
Kind field on shippedPreset: an entry still identified by preset_id
"openai", our base URL, the shipped catalog, window 1,050,000 and default
gpt-6-sol is moved onto the Responses wire. Anything its user changed in
that shape is left alone, and a second pass changes nothing.
- A consistency test walks every curated preset: a model whose table row
names its wires must be served on one of them, and declared efforts and
default must sit inside that model's ladder. The old preset fails it for
all three GPT-6 models.
- Effect tests through boot.Build hold a fresh install and an install of the
old shape to a Responses body carrying tools and reasoning.effort=medium.
Docs: https://developers.openai.com/api/docs/guides/latest-model,
https://developers.openai.com/api/docs/models/gpt-6-sol,
https://developers.openai.com/api/docs/models/gpt-6-luna,
https://developers.openai.com/api/docs/guides/reasoningCloses#11199
The agent's browser panel put its page away and showed "the page is hidden
while something covers it" when nothing a person could see was over it, and
sometimes stayed that way after the cover left.
Why it misfired:
- The check took the topmost element at nine points as the answer. A layer at
opacity 0, or a transparent full-window layer that only catches clicks, is
the topmost element and paints nothing, yet it hid the whole page.
- A cover that leaves by a CSS transition, an animation or a hover changes no
attribute the panel watched, so the page stayed away until something
unrelated moved. The observer also only listened to five attribute names.
- A popover reaching into one corner hid the whole page and replaced it with
a sentence.
What changes:
- ui/occlusion.ts decides from hit testing plus computed style and geometry:
an element counts only if it paints at the probe point (effective opacity,
visibility, background, backdrop filter, a border band, its own text rects,
or a replaced element). The slot's own content, its ancestors, and layers
that paint nothing are skipped. The rectangle sent to the shell is the part
of the slot the window shows, cut to clipping ancestors. A layer reaching no
more than 8px into the edge, like a column gutter's grip, does not count.
- Recovery no longer depends on a mutation: transition and animation
end events reschedule the check, all attributes are observed, and a 400ms poll
brings the live page back whatever moved.
- A covered page is frozen instead of hidden: the shell takes a picture of the
page on screen with capturePage before putting it away, and the panel draws
that picture in the slot until the cover leaves. The sentence is gone.
The picture channel (browser:freeze) takes only the page already shown in
this window, answers only this window's own page, and the picture is drawn in
an <img> and sent nowhere else. A freeze overtaken by a show does not put the
page away. An older shell without the verb falls back to hiding the page.
Tests: browser-panel.interaction.test.tsx covers invisible, transparent and
hidden layers, the slot's own content, recovery with no DOM change, stale
pictures and view transitions. test/freeze_live.js checks the channel in a
real Electron window.
A relay that serves several vendors' models under one address and key could
only declare one supported_efforts list for all of them. Each model can now
carry its own, stored where per-model settings already live
(model_overrides.<model>.supported_efforts / default_effort) and applied by
the same model resolution every request and every level picker reads, so a
level the picker offers is the level the request carries.
Precedence: the model's own list, then the connection's supported_efforts,
then the model table for the endpoint, then the protocol's generic ladder. A
model with no declaration inherits; installed configs need no migration.
- config: ModelEffortDeclaration / SetModelEffortDeclaration (keeps every
other override field, drops an emptied override, ErrModelDefaultEffortNotListed
for a default outside the levels) and InheritedEffortCapability. ResolveModel's
three branches share one forModel step.
- EffectiveEffort: the stored level is one per connection, so after a model
switch it can fall outside the next model's levels. That model now runs at
its own default, the level its picker's "auto" names, instead of omitting
the field while the picker says auto means something else.
- An override that only sets max_output_tokens is no longer dropped as empty
on load.
- serve: /providers lists each chat model's own, inherited and protocol
declarations; /providers/edit takes modelEfforts as the whole answer for the
listed models, refusing with provider.model_default_effort_not_listed and
provider.model_effort_unlisted.
- Studio: the "Thinking and reasoning levels" fold gains "Levels per model":
inherit (showing what that means) or pick the model's own levels and default.
The effect test builds a mixed relay through boot.Build and checks the
request body for every offered level of both models, and that a level a
model does not take never reaches the wire.
Closes#11192
The check that refuses a click as browser.covered answered a different
question from the one it was asked, in four ways:
- DOM.getNodeForLocation takes document coordinates, and it was given
viewport ones. On a page scrolled by less than the target's distance
from the top of the viewport it hit-tested a point that far above the
target and reported whatever sat there (a banner, a nav bar) as
covering it; scrolled further, the lookup found nothing and the check
passed, so a click under a fixed header or a veil went through to the
wrong element.
- ignorePointerEventsNone: true makes the hit test count
pointer-events:none elements as hits, the opposite of what a pointer
does, so decorative overlays were reported as covering.
- Newer Chromium (Edge 154) resolves a hit on a ::before/::after to a
CSSPseudoElement, which has no parentNode, so a button with a ripple
layer was refused as covered by its own pseudo-element.
- A form control drawn by its label (a zero-size or visually hidden
checkbox, a radio painted over by a span in its label) was refused as
not_visible or covered, although a person clicks the label.
The hit point is now converted to document coordinates, pointer-events:none
is left to the browser, a pseudo-element hit counts as its element, a hit
inside one of the target's labels counts unless it passes through other
interactive content (a link in the label follows the link), and before
refusing, the element is scrolled to the middle of the viewport, which
clears fixed headers and footers. A real cover is still refused, now
naming what is on top by ref. The label list comes from the page, so it is
capped and checked to be labels on the host side.
Measured with a table of pages at 1x and 1.5x device scale on Chromium 151
and Edge 154: before, 6 false refusals (7 on Edge), one click silently
landing on a fixed header and one passing through a veil; after, none,
and the true covers are still refused.
Every curated preset so far points at a Chinese coding plan or a
multi-model relay, so a user arriving with an OpenRouter, OpenAI or
Gemini key has to write the provider by hand.
- openrouter: DeepSeek, GPT-6, Gemini, Kimi, GLM, Qwen and MiniMax ids
checked against /api/v1/models; credits balance via the existing
OpenRouter decoder; HTTP-Referer / X-OpenRouter-Title /
X-OpenRouter-Categories so the traffic is attributed to Reasonix in
OpenRouter's app rankings. Claude ids are left out: on the openai
dialect no cache_control is sent, so every prompt token would bill at
the full input rate (claude-dialect).
- openai: GPT-6 Sol/Astra/Luna on Chat Completions, which already sends
max_completion_tokens to api.openai.com; declares the documented
low..max effort ladder.
- gemini: the Developer API's OpenAI-compatible endpoint.
- volcengine-coding-plan: the Coding Plan endpoint with ark-code-latest,
which follows the model chosen in the Ark console.
The presets live in their own file; provider_presets.go only appends
them.
A model can now declare optional run modes beside its effort ladder, in the
same model capability table. Each declaration names its id, the frontend's
label/hint keys, how it maps onto the request, the wires (kinds) and the
vendor endpoint it was established on, and whether it costs noticeably more.
GPT-5.6 (gpt-5.6 alias, sol/terra/luna) and GPT-6 (sol/luna/astra) declare
"pro", sent as Responses reasoning.mode "pro" beside reasoning.effort, on the
Responses API at api.openai.com only (developers.openai.com/api/docs/guides/
reasoning: mode and effort are independent; pro does more model work and is
billed at the model's rates for more tokens).
The mode is session state, not config and not prompt text:
- control.Controller lists the modes the session's model declares and sets
one; an undeclared mode is refused with ErrModelModeUnsupported.
- The agent carries the chosen id on every main-loop provider.Request; a new
conversation starts with it off. Subagents, compaction and side models
never carry it.
- The Responses adapter sends reasoning.mode only for a mode its endpoint
declared (projected by boot into the provider config), so a relay, Chat
Completions or a model without the declaration never sees the field.
- A rebuild carries the mode onto a model that declares it and drops it
otherwise, so a model switch turns Pro off.
- serve: POST /model-mode, refused as model_mode.unsupported; /status lists
modes only for a model that declares any.
Studio shows a switch at the bottom of the effort menu only when the model
declares a mode, marked as costing more; the trigger reads "... · Pro mode"
while it is on. The row stays pinned to the bottom of a scrolling menu.
Documents how Reasonix discovers and runs Agent Skills: the project,
custom, global and built-in roots, run_skill and slash invocation,
references/ and scripts/ handling, the extra frontmatter keys, and the
/skills and [skills] controls. agentskills.io asks listed clients to link
a page describing their implementation; this is that page.
Persist always-allow rules in private per-workspace user state and load them into the effective policy. Default skill installs to user scope unless project scope is requested, so neither action creates reasonix.toml in the workspace.
The model table named GPT-5.6's none..max ladder, but the provider was only
ever told a user-declared supported_efforts list, so on api.openai.com the
request builder refused "none" and "xhigh" at build time and clamped "max" to
"high". RequestEffortLevels now projects the resolved ladder (declared list
first, then the model table for the endpoint in hand) into the provider
config, so every rung the composer offers reaches the wire as itself.
Ladders, each from the vendor's own documentation:
- gpt-5.6 (alias of gpt-5.6-sol), sol/terra/luna: none|low|medium|high|xhigh|max,
default medium, on Chat Completions and Responses.
- gpt-6-sol / gpt-6-luna: none..max, default medium; gpt-6-astra: low..max
(none answers 400). Responses only: on Chat Completions GPT-6 takes
function calls only at reasoning_effort=none, and every turn carries tools.
- Model Studio Qwen3.8 (qwen3.8-max, -max-0902, -flash, -2.4t-a95b, -27b):
low|medium|xhigh plus none (enable_thinking=false), default xhigh; high and
max fold to xhigh, minimal to low. Vendor hosts: dashscope(-intl).aliyuncs.com
and <workspace>.<region>.maas.aliyuncs.com.
A ladder's vendor is now a host set with suffixes (vendorEndpoint), and a
ladder names the provider kinds it was declared for. The model table moves to
effort_models.go. The composer names none ("No reasoning") and tells xhigh
("Extra deep") apart from max, which shared one label.
reasoning.mode "pro" is not added here: it is a Responses-only dimension
independent of effort and needs its own control.
approval.go went 10 lines over repolint's 800-line ceiling with the MCP
origin handling. The ask-notification text builder now sits beside its
existing test file; no behaviour change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The rewind menu's three scope labels and the bash coverage-gap reason are
Chinese literals held in a table and rendered as written:
<span className="lb">{s.label}</span>
Neither static guard could see them. The catalogue test only collects
Chinese inside a t() call, and the JSX test only reads text between tags or
in a rendered attribute. A table holding Chinese is correct exactly when its
render site calls t(), so the render site was the thing that was wrong. The
browser sweep that is meant to be the judge never opens this menu either:
tools/steps.mjs walks no rewind step.
Route both render sites through t() and add the four keys to the English
catalogue. A Chinese window is unchanged - zh still falls back to the key.
Verified: pnpm typecheck; pnpm test (169 files, 1035 tests).
Extract ProjectConfigPath(root) as the single place the project config
path is computed, and route every caller through it: load, credentials,
edit, retired-key migration, MCP migration and boot. It still resolves to
<root>/reasonix.toml, so behaviour is unchanged.
MigrateV2 rewrites a fact and then the index. When the index is unreadable,
reindexIn fails after the fact has already been rewritten, and the next run
skips facts that carry an id, so the entry stays out of the index for good.
The migration now reads the index once per directory before touching any fact,
the same guard RestoreArchived uses, so a failure happens before the first
write and a retry still sees the fact as unmigrated.
Cache note: no prefix bytes move; this only decides whether a migration starts.
Add a user-global [tools.shell] env table so every command the bash tool
runs inherits extra KEY=value variables (for example
`BASH_ENV = "~/.reasonix/bashsandbox.rc"`). The value is applied over the
inherited environment but under the host-owned session/egress overrides, so
a preset value never displaces TMPDIR or an egress token.
The variables ride the sandbox spec (sandbox.Spec.ShellEnv), so foreground,
background, and host-terminal runs all inherit them alike. It is
user-global: a cloned repo's reasonix.toml cannot inject variables, because
the shell env is held across the project merge next to the sandbox grants.
Presetting a variable that names a host endpoint (SSH_AUTH_SOCK, DOCKER_HOST,
CONTAINER_HOST, XDG_RUNTIME_DIR) does not unmask it: the denied authority set
resolves from both the host environment and the effective one, so a preset
that points a client elsewhere still leaves the host's own socket masked. The
egress loopback proxy ports are unioned the same way, so a preset proxy is
closed and overriding a variable cannot reopen the host's.
The interpreter itself is still resolved by [tools.shell] prefer/path. A key
that cannot appear in an environment entry is dropped rather than written as
a malformed one, and keys are sorted so identical config yields identical
environment bytes.
The recall diagnostic reported one Omitted count for two different decisions:
the per-turn hit cap and the character budget. The note the model reads already
names both, so a reader could not tell which one dropped a fact.
RecallResult now reports OmittedByLimit and OmittedByBudget apart, and the
/memory recall diagnostic prints both. The trajectory audit keeps its combined
Omitted value, so the persisted shape is unchanged, and the model-visible note
still carries the sum of the two.
From #11146's review: "Splitting omitted into limit vs budget would be a nice
separate diagnostic."
Address the two non-blocking nits from #11148's review: state the constraint
the test pins instead of narrating the implementation, and assert the error is
the pinned-budget refusal rather than any error.
The assertion matches on identity rather than on the message:
validatePinnedBudget wraps ErrPinnedBudget, and both the recovery test and the
save path's own test assert with errors.Is. The sentinel's text is the setting
the message already names, so the user-visible message stays byte-identical.
Between roughly 470px and 720px of conversation column (a wide browser
pane is the usual way there) every toolbar control was flex: none, so the
model/effort group ran past the toolbar's box: the send or stop button sat
on the effort picker and the effort label painted outside the composer.
Below 470px the send cluster was taken out of flow with a fixed 38px
reserve, which the wider stop button does not fit in.
The row now lays out in flow at every width: the send cluster keeps its
own slot at the end of the line, the toolbar wraps inside what is left,
and the model group has a minimum basis, grows to fill the line and lets
the model name shrink to an ellipsis before it moves to a line of its
own. The spacer and the per-width offsets it needed are gone.
perf/composer.mjs sweeps 340-760px idle and running with a long model
name and fails on any control outside the composer, any two controls
overlapping, or a model name narrower than 40px; it fails on the old
stylesheet at 340px (running) and 480-720px (both states).
Fixes#11159
A run that keeps calling tools without changing anything the host can
observe (#11153: dozens of successful `echo` calls with varying arguments,
about 1.1M input tokens) had no signal short of reading the transcript.
This adds one, without bringing back any round-count guard: nothing is
sent to the model, no call is refused, and the cache-stable prefix and
the per-turn execution-policy block are untouched.
Judgement (kernel, structure only):
- evidence.EffectWatch scores each round's receipts. An effect is a
successful mutation or named write, a task step moving (StepProof,
TodoStep, or the host's todo progress counter), a verification whose
outcome changed since it last ran, a first read (a read-only tool
answering a request not seen before in this run, or a shell command
naming an existing path not named before, resolved by stat), or a
delegated result coming back. A command merely being new is not.
- agent.settleProgressRound runs at each tool-round boundary; the count
lives in turnRuntime, so a user message zeroes it. A second,
model-independent backstop compares the input tokens spent since the
last round with an effect against context window x N; an effect
restarts both counts, so a long run that keeps doing observable work
is never flagged however much it spends. Without a ledger the round
judgement stays silent rather than guess.
Feature 2 (default on): a content-free progress_watch event carries the
reading; Studio shows a one-line strip above the composer with Stop
(the ordinary cancel) and "don't show again (this session)". It clears
itself when an effect lands or the turn ends. The TUI says it once per
stall.
Feature 3 (default off): [progress_watch] pause = true ends the run at
the round boundary through a resumable pause (RunPauseInfo Kind
no_progress, TurnDone outcome no_progress). No finalization nudge is
injected. Continue sends an ordinary message, which starts a new run
with the count at zero. The section is user-level only: load restores
the user's value over a project reasonix.toml. Settings > Session edits
it through GET/POST /progress-watch, which persists to the user file
and hands the new watch to the running executor without a rebuild.
Out-of-range values refuse with progress_watch.out_of_range.
Refs #11153
The market entry gains up/down vote buttons that show the signed-in
account's vote; pressing the lit one withdraws it. Signed out, the entry
offers sign-in (a jump to the account section); a publisher, an
unverified account and a paired device each say why they cannot vote.
List rows show approval and install count, and the listing defaults to
the registry's new recommended order.
The kernel owns the calls: market.Client gains MyVote/Vote, which send
the account token only to the fixed registry host with redirects
refused, and ReportInstall. Registry refusals keep their identity
(signed_out, email_unverified, own_package, rate_limited; an identity
outage stays market.unreachable) through market.* refusal codes. Vote routes sit
behind accountAuthAllowed like every other account route.
A successful market install reports once, carrying only the slug and a
random id kept only for the market (market/install-id under the Reasonix
home, unrelated to the usage-statistics id), and only when the host allows
it (release builds), both desktop statistics switches are on, and
neither DO_NOT_TRACK, REASONIX_TELEMETRY=off nor CI opts out.
The [desktop] telemetry and metrics switch descriptions now say that
per-package market install counts are reported (anonymous, no content).
A publisher's own unreviewed install is never reported.
The usage panel summed every turn in the currency its price table was
kept in, and ignored the display currency the rest of Studio reads
costs in. A DeepSeek entry holds the vendor's USD table, so a user who
pinned CNY, or whose CNY wallet selected it, saw "US$" on the panel
while the pane's cost meter said "¥".
Every quote already carries the vendor's own table in both currencies,
and the stats line records both valuations; nothing had to be converted,
only read. The usage catalog now keeps each row's valuations and tallies
one partition per view: a row the vendor priced in the display currency
moves into it, and any other row keeps its billed currency, so a relay's
USD price is still shown as USD rather than converted at an invented
rate. The schema version moves to 5, which rebuilds the projection from
the JSONL it is derived from.
The live projection path also dropped the cost of every row a running
process appended, marking the file indexed all the same, so today's
spend vanished from the panel until the next restart. It now carries
the same cost a reindex would.
A skill in the Studio composer was only the text "/name", and only a
leading one ran: the kernel reads a slash at the start of a line and
nowhere else, so "/review" typed mid-sentence reached the model as prose.
Picking a skill or subagent from the slash menu now places a chip, at the
start of the line or after any "/" that opens a word. A "/" past the start
asks the kernel's completion as if it opened the line and keeps only
skills and subagents, since a command is not something a sentence can
hold. The chip is the record of the pick; the same letters typed by hand
stay prose, so nothing reads the submitted wording to find a skill.
The chip is drawn by a mirror laid over the textarea at the textarea's own
computed metrics, so caret, selection, IME and undo stay native. Edits are
reconciled from the text the chips were last placed in, so every way into
the box (typing, drops, prompt refine, a refused send) carries them, and an
edit that reaches into a chip removes it whole. Skills take --net and
subagents --deleg.
On send the line goes out as typed (input), the text with the chips taken
out (submit), and the chips (invocations). /submit and POST /inbox/items
route a chip line through the inbox envelope, which already carries
invocations to prepareInvocationTurn, so a mid-line chip reaches the model
byte-identical to a leading "/name task". A kernel that predates this reads
only input and still runs a leading chip. TryEnqueueAndSteer queues a line
holding an invocation as a follow-up: a steer is read as guidance and would
drop the skill.
Closes#11145
RestoreArchived checked id and name collisions but skipped the rest of what a
save validates: a recovered fact could take a subject_key another active fact
now holds, or push pinned guidance over the configured budget. The fact model
states the subject rule unconditionally, and the save path refuses both.
Validate the restored fact with the same validator SaveWithOptions uses, before
any part of the restore is written.
studio still drops the read error when it rewrites the MEMORY.md index:
store.go reads the existing index for indexLinesExceptIn and flushIndexIn
with `existing, _ :=`, so an unreadable index reads as empty and the next
save rebuilds it from the managed lines alone, silently discarding
hand-written content while reporting success.
main-v2 already fixed this in 2eb878989 ("preserve index when reads fail"),
with 79acaf6e9 moving the readers into store_index.go to stay inside the
repolint budgets. This ports the same shape to studio so both lines behave
alike:
- readIndexIn keeps treating a missing index as empty but returns any other
read error; indexLinesExceptIn reports that error and whether the name was
present, which lets indexContainsIn go away.
- flushIndexIn and reindexIn propagate the read error instead of rebuilding
the index from an empty read.
- archiveLocked and archiveMemoryInDir read the index before archiving, so a
failed read happens before any side effect.
- SaveWithOptions reuses removeMemoryFromDir and archiveMemoryInDir, dropping
the duplicated remove/flush and archive/flush pairs and keeping the
function inside its size and complexity budgets.
AppendDoc discarded the read error, so an existing but unreadable doc-memory
file (REASONIX.md/AGENTS.md) was treated as empty and rebuilt from scratch,
silently destroying the user's standing instructions. Surface the read error
unless the file is simply missing.
Ported from 1.x (#8319), which fixed the same defect on main-v2; the 2.x
writer still had the original discard.
Entries may carry a discussion number instead of an issue number. Discussions
have no REST comment API, so the reply goes through addDiscussionComment and
skips a body the bot already posted there.
A publisher can now keep a package to themselves and install their own
packages before, or without, review.
- Publishing takes a visibility. "Only me" saves the package as private:
it skips review and never appears in the market. My packages marks it
private and offers "Submit for review", which moves it to pending.
- Every row in My packages can be installed. The kernel reads the package
from the registry's /v1/me/packages/:handle/:name as the signed-in
account, never from what the window sends, so another account's package
is refused (market.not_yours).
- A live version keeps its reviewer's pin. Any other version has none, so
the preview's own contentDigest becomes the pin: apply must echo it
(market.unpreviewed otherwise) and install_source refuses material that
changed since the preview. The confirmation says "Unreviewed, visible
only to you", and the ledger records the install as unreviewed.
- The plan, the risk grades, the multi-skill confirmation and the
themes-only check are the same ones every market install passes; the
confirmation view is shared with the market entry page.
Each market row is now two lines: name, kind, verified/installed badges and
the install count on the first; a one-line truncated summary with the
publisher and version, de-emphasised, on the second. Rows stay buttons, so
they open with the keyboard, and truncate rather than overflow at phone width.
The listing now says which packages can be installed from the market. The
kernel reads the registry's `pinned` per listed package (nil when an older
registry does not say, so nothing is claimed) and forwards `pinned=1` for the
new "Installable only" toggle. The registry applies that filter, so pages stay
whole; a registry that answers a filtered ask with an unfiltered page is
refused as market.filter_unsupported rather than trimmed client-side. The
install path is unchanged and still checks the approved version's digest.
The chunked update stages the new tree in the update cache and a helper
swaps it in after exit, renaming each installed file into a backup
directory beside the staged tree. Two installs can never complete that
first rename: an all-users install under Program Files, which grants its
users read and execute only, and an install on another volume than the
user's cache, where MoveFileEx without MOVEFILE_COPY_ALLOWED refuses.
For both, the helper rolled every file back and relaunched the old build
with nothing shown, so the update downloaded, "installed", and the
application came back on the version it started from. Before chunked
updates these installs took the full installer, which raises its own
consent and writes wherever it installed.
tryDelta now proves that move before fetching anything: it creates an
entry in the install root and renames it into the cache's delta
directory. If either step fails the full package is downloaded instead,
as for any other delta that cannot be used.
Closes#11115
In a conversation column of 460px or less, app.css stretches the send
cluster to width: 100%, and at 470px or less studio.css takes the same
element out of flow (position: absolute; right: 0; bottom: 0). The two
rules combined into an invisible full-width strip over the composer's
last line, so every tap on the model picker or the effort picker landed
on the send cluster. The pickers rendered but never opened on a phone,
over LAN and over the relay alike.
The absolute rule now declares width: auto, so the cluster is only as
wide as the send button. A stylesheet test resolves the cascade for
the cluster at phone widths and fails if it is out of flow and a full
row wide.
Closes#11116
An over-budget worktrees/ or modules/ group was bound read-only as a whole,
but the exact mounts for the gitdirs inside it were still emitted first, so
planting submodule gitdirs kept growing the bubblewrap argument list. Paths
beneath a read-only tree are now left to that tree.
The serve launch-token sandbox test placed its files under /tmp outside the
write root, which bubblewrap masks with a tmpfs: the control read failed and
the denied reads passed without exercising the forbid-read mounts. The files
now live inside the write root on every platform.
Add English wording for the serve.host_rejected and
auth.launch_token_required refusals.
Signed-in users can now submit skills, plugins, MCP servers and themes to
the community registry from Settings > Extensions > Discover, and follow
each submission's review state.
- internal/ext/market: Client.Publish (POST /v1/packages) and Client.Mine
(GET /v1/me/packages) send the account session as a Bearer token to the
same fixed https host the listing reads, with redirects refused, so the
token never reaches another host. The registry's refusal codes are
projected onto sentinels (ErrSignedOut, ErrEmailUnverified, ErrNotOwner,
ErrVersionExists, ErrRateLimited, ErrRejected).
- A submission is checked against the same source rule an approved
version must pass to install (sourceInstallable, shared with
installable): plugins and themes must name a GitHub tree pinned to a
full commit. Nothing is sent for review that could never be installed.
- theme is its own listing kind that installs through the plugin
installer (Installer("theme") == "plugin") under the same commit pin and
digest check. The plan must consist only of plugin packages that carry
themes and nothing else (no skills, commands, agents, hooks, MCP servers,
prompts or runtime); otherwise market.not_theme.
- serve: POST /market/publish and GET /market/mine, gated like every
account route (host grant plus a stored session), each failure a dotted
code (market.signed_out, market.unpublishable, market.rejected, ...).
- Frontend: Browse / My packages / Publish views inside the market group
while signed in, a sign-in prompt otherwise; a Theme category filter.
- A theme's apply re-plans and checks themes-only before writing, because
a planId binds the source and actions but not the listing kind. A
registry accounts_unavailable answer reads as market.unreachable, not
as signed out.
The host's own git reads the workspace repository's configuration and hooks,
so a confined command that can rewrite them can make the host run a program
of its choosing. The OS sandbox now denies writes to them while leaving the
rest of .git writable.
For the repository git itself discovers from each writable root, protected
paths are: config, config.worktree, commondir and hooks/ of the gitdir and
of the common dir; config, config.worktree and commondir of every existing
linked worktree entry (new entries stay creatable); config, config.worktree,
commondir and hooks/ of every submodule gitdir under modules/. A .git file is followed to the gitdir it names
exactly as git resolves it, and the pointer file and commondir are
protected, so a repository cannot aim the protection at a decoy while git
reads something else. The .git entry, the gitdir, the common dir, every
symlink on the way to them and each writable ancestor are pinned: they
cannot be removed, renamed away or replaced by a symlink, while their own
mode and times stay writable (touch and chmod on the workspace root work).
- macOS: deny rules after every write allowance. Existing worktree and
submodule gitdirs (up to 128 each, found without following symlinks) get
exact rules and pins; patterns anchored at the common dir cover the rest
and gitdirs created later, skipping refs/ and logs/ segments so a branch
or tag named config or hooks stays writable. The profile stops growing
past the limit.
- Linux: pinned directories become their own bind mounts and protected files
and hook trees are bound read-only; only real directories and regular
files are mount targets. Up to 512 gitdirs per group get exact mounts;
past that the whole group directory is bound read-only, so planted HEAD
files cannot overflow the argument list. Only existing paths can be
mounted and symlinks cannot be pinned.
- Windows has no Bash sandbox; the guide says so.
A protected file with another hard link refuses the confined launch with
sandbox.git_metadata_linked (ErrGitMetadataLinked), since no path rule sees
a write through the other name. Whenever a confined command's output names
a protected path, also on exit 0 (git reports some refused config writes
and exits 0), the bash result carries sandbox.git_metadata_protected.
Real-sandbox tests cover each protected path, pointer and symlink
retargeting, patterns for gitdirs created later, bounded rule size with
2000 submodule gitdirs, ordinary git work, and the git operations that now
fail or silently skip their config write.
Every git invocation the host makes goes through gitcmd, and each one must
leave the programs a repository's own configuration names unexecuted:
filter and merge drivers, textconv and external diff, fsmonitor, hooks,
signature verifiers, submodule-boundary programs and lazy-fetch transport.
- Fixed keys are overridden with -c: core.fsmonitor (empty, not "false",
which git before 2.35.2 reads as a hook name), core.hooksPath,
log.showSignature, merge.verifySignatures, diff.submodule, gc.auto and
maintenance.auto and safe.bareRepository=explicit; submodule.recurse=false
for checkout, switch, restore, reset, merge and read-tree (host tree
updates never update submodules), and elsewhere only where the
repository's own config sets it.
GIT_NO_LAZY_FETCH=1 stops a missing object becoming a fetch through the
repository's remote (git 2.44+); a historical patch that fails for that
reason returns gitcmd.ErrObjectNotLocal, decided by git's own
--missing=print listing.
- diff, log and show carry --no-ext-diff and --no-textconv; status and diff
carry --ignore-submodules=dirty. The subcommand is found past global
options, so a call passing -C inside args is hardened too.
- Filter and merge drivers defined at local or worktree scope are listed by
git itself before each invocation (includes followed) and overridden empty;
user and system scope stay live; a name -c cannot address fails closed
(ErrRepositoryDrivers).
- worktree add uses --no-checkout and populates through gitcmd inside the new
worktree, so a conditional include keyed on the new gitdir or branch is
seen by the driver listing there rather than run by the add's checkout.
The reset takes the worktree-mutation timeout, and a failed reset removes
both the worktree and the branch the add created.
- Launcher ls-remote runs Detached with gitcmd's environment (only PATH is
taken from the launcher's declared env), requires an https://, ssh:// or
git:// remote (case-insensitive; http:// and local paths are refused), and
passes the remote and ref past -- so a leading-dash value is an operand.
- frontend/cli runGit passes its directory through gitcmd so enumeration runs
in the target repository, not the process cwd.
- Repository identity is resolved once, when a session opens: boot runs
gitcmd.Open (rev-parse --absolute-git-dir --git-common-dir --show-toplevel)
before any command, the controller carries the resulting gitcmd.Repo, and a
rebuild of the same workspace passes it on. Every later host call runs
through the Repo with GIT_DIR, GIT_COMMON_DIR and GIT_WORK_TREE set, so a
nested .git, a commondir file or a damaged HEAD written afterwards cannot
substitute another repository or its config; a damaged HEAD fails closed.
Threaded through gitstatus Status/Diff (serve /changes and diff),
worktree Inspect/Create/TakeSnapshot and the candidate/apply/remove paths
(Snapshot and Candidate carry their Repo; host-created worktrees are opened
right after worktree add), the isolation store, best_of_n, and cli review
(resolved once before the review agent runs; ObjectsMissing is a Repo
method).
- Open accepts only a git dir belonging to the work tree that holds the
directory: the work tree's own .git (sharing no other common dir) or a
linked worktree's admin dir under <common>/worktrees whose gitdir file
names that work tree's .git. A .git file or core.worktree pointing at
another checkout is ErrNotRepository. Submodule checkouts and
--separate-git-dir repositories opened directly are therefore treated as
not version-controlled.
- Repo.Verify applies Open's test to a stored identity; Snapshot and
Candidate identities are verified when used again.
- The snapshot's private index copy keeps the real index's mtime: with a
fresh mtime git trusted stat for entries written in the index's own
second, so a same-size edit made in that second was left out.
- Snapshot staging uses gitcmd Repo.StageAll: gitlinks present as directories
and untracked nested repositories are excluded with :(exclude,literal)
pathspecs, because add -A checks a submodule by running git inside it under
its own config. A gitlink keeps the commit the index records; an untracked
nested repository is left out (it previously failed the snapshot when it had
no commit).
Regression tests use real git with payload scripts: the gitcmd config-key
matrix (signatures, lazy fetch, submodule boundary), the worktree call sites
(conditional includes on Create and CreateCandidate), the launcher's non-URL
remote rejection, identity pinning after a nested .git, a commondir file and
a damaged HEAD (config origin and filter marker), Open refusing another
checkout's git dir (.git file, core.worktree, foreign admin dir), StageAll
with nested repositories named like pathspec magic or globs, StageAll and TakeSnapshot
past a gitlink with its own filter, the listing and snapshot of a workspace
that later gains its own .git, and a global submodule.recurse=true against
host checkout and reset. The list-then-run race is left to a follow-up that
denies sandboxed writes to .git config and hooks.
`reasonix serve` now generates a per-launch token unless `--auth` or
`[serve].auth_mode` chooses otherwise. `none` remains an explicit opt-out for
deployments fronted by their own authentication: reads stay open to loopback
and bound names only (421 `serve.host_rejected` otherwise), and every
state-changing request, approvals included, needs the launch token (bearer
header or the /auth/token cookie) and otherwise refuses with
`auth.launch_token_required`. A request the loopback gate, a paired-device
gate or the in-process transport already admitted counts as holding it, so
Studio and the TUI keep working unchanged. Token mode also accepts the bearer
header.
The token never reaches the terminal: serve writes it to a 0600 file under the
remote state directory and prints the path, or names the --token-file of a
managed launch. Runtime sandboxes and read tools deny that directory and every
`--token-file`, `--provider-broker-file` and `--provider-broker-token-file`.
[serve] is read from the user config only, so a project reasonix.toml cannot
choose the token, the auth mode, or trust in forwarded headers.
Studio can now browse, search and install from the community registry
(Settings > Extensions > Discover). The market is a source, not a trust root:
- internal/ext/market reads the registry read-only: https to the fixed
crash.reasonix.io host, 15s timeout, capped bodies, redirects refused,
non-active rows dropped. Registry text is untrusted data.
- Only the approved version is installable, and only when its row carries a
content digest. The digest travels to install_source as expectDigest.
- install_source gains a contentDigest on every plan (machine-independent:
SKILL.md bytes, plugin commit, MCP entry) and refuses a plan whose material
differs (ErrDigestMismatch) or cannot be pinned (ErrNotPinnable). Preview
and apply both pass the check, and apply writes what its own plan read.
- GitHub plugin sources pinned to a full commit (tree/<sha>) now clone and
check out that commit instead of failing on --branch <sha>.
- A remote source that expands into more than one skill is graded high, so
the permission layer asks a person even under a blanket allow, and the
market confirmation lists every skill and holds the install button until
the person ticks that they read them.
- Market installs require the planId of the plan that was shown, go to the
global scope, and are recorded in <home>/market/installed.json; "installed"
is read back only while the recorded targets still exist.
- serve: GET /market/packages, GET /market/packages/{handle}/{name},
POST /market/plan, POST /market/install, each failure a dotted code
(market.unpinned, market.content_changed, market.unreachable, ...).
Adds an opt-in cloud backup under Settings > Account, shown only while
signed in. The kernel collects the user-level setup by category, seals
it locally (gzip, then XChaCha20-Poly1305 under an Argon2id key derived
from a passphrase the user sets), and uploads only the envelope to the
accounts service; the service never sees plaintext.
Categories: settings (providers, default model, interface look),
extensions (user skills, plugin references by source and version, MCP
servers), memory (user-level instruction files and global facts),
automation (global hooks, status line) and secrets (stored provider
keys, off by default). Without secrets, literal env/header values are
stripped with the plugin exporter's StripCredentials and URL userinfo
is dropped; the account session token never travels. Approval mode,
permissions and sandbox are deliberately not restorable.
Restore is preview-then-apply. The preview decrypts, diffs each item
against this machine, shows everything an item would write (endpoints,
env, headers, cwd, instruction file bodies) and holds the snapshot under
a single-use plan id. Apply recomputes consent against the machine at
apply time and refuses, writing nothing, unless each of these is named
in `consented`: hooks, the status line and MCP servers (code that runs
here); a provider that is new or changes endpoint, and a default model
pointing at one (conversations and keys go there); a key that replaces
one already stored; an instruction file that imports other files.
A snapshot is rejected outright when a key is not read by a provider it
carries, when a provider reads REASONIX_* or a host-steering variable
(proxy, TLS, loader, PATH), or when a unit's payload names another item.
Plugins are never installed by restore; they come back as sources for
the ordinary plugin install flow and its plan approval. File items are
written only under their named root, through validated relative paths
that may not cross a symlink; hidden files are not exported. Config
writes go through config.EditConfigFile, hooks through hook.Save, keys
through config.SetCredential.
Serve: GET/POST /backups, DELETE /backups/{id}, POST /backups/{id}/preview,
POST /backups/apply, gated by the account grant and a signed-in token,
with dotted refusal codes the frontend words in both languages.
The hosted build has no kernel behind its origin, so opening
studio.reasonix.io directly probed /runtimes, got the SPA shell and
reported that the kernel did not answer. The deploy now declares where a
visitor picks a computer, and a visit without ?device goes there.
A plan whose steps name candidate files owes a change the host proved.
The shadow verdict and the receipt already said so; the gate now holds
an answer that changed nothing, and releases a turn that concluded
blocked, is waiting on the user, or carries a change a restart brought
in. The hold names all three ways out.
The executor-handoff nudge now runs before the gate ends the run: an
executor that has not acted yet gets its one in-turn nudge first, and
the gate holds only what the nudge could not recover.
publish downloads studio-windows-signed by name, but any job in the run can
upload an artifact. windows-sign-installer now outputs the SHA-256 of the
installer it signed and verified, and publish requires the downloaded
artifact to hold exactly the installer and the portable zip, hashing to
that output and to windows-verify-package's archive hash. A mismatch, a
missing hash or an extra file fails with a studio-signing.* title before
anything is signed or published.
The Microsoft-signed d3dcompiler_47.dll and dxil.dll must now also chain to
Microsoft Root Certificate Authority 2010, pinned by thumbprint; subject and
issuer names alone can be issued under any trusted root.
The plan-criteria gate and the frozen contract verdict measured "passed
after the latest change" from LatestSuccessfulMutationIndex, which counts
an unclassified command as a change. A plan check such as `python -c ...`
is exactly such a command, so it became the latest change and owed itself
a later run that could never come: every plan whose checks were inline
scripts ended with its criteria outstanding, graded pass or not.
Both now read LatestProvenMutationIndex, the baseline project checks and
owed review already use: the latest proven write, or the latest unproven
one only when nothing was proven.
The Windows release signed only the three executables Studio builds. The
installed tree also ships Electron's ffmpeg.dll, dxcompiler.dll,
vk_swiftshader.dll and vulkan-1.dll unsigned, and Smart App Control / WDAC
can refuse to load an unsigned low-reputation DLL, so the app may not start.
Every PE file the Windows bundle may hold is now declared in
scripts/windows-signing-lib.ps1: seven the release signs, and
d3dcompiler_47.dll and dxil.dll, which Electron ships signed by Microsoft
and which are kept that way and verified as Microsoft-signed. A bundle with
an undeclared or missing PE file (.exe, .dll, .node, or any MZ header) fails
until a person updates the list.
windows-sign-payload signs the declared files, verifies all nine, and
records a SHA-256 digest of the whole signed tree as a job output. The
signed tree is the payload artifact; windows-package packages it as it
came. A new job, windows-verify-package, with no secrets and no
environment, checks the payload against that digest, unpacks the zip and
the installer's app-64.7z, requires both trees to equal the payload file
for file (plus resources/elevate.exe, pinned by SHA-256), and outputs the
SHA-256 of both packages. windows-sign-installer only requires the packages
to hash to those values before signing, and afterwards only runs SignTool
verification, so no archive is parsed in a job holding the Certum
credentials and Certum never signs an installer whose contents did not
match.
Signatures are read from the file itself through SignTool and the embedded
signer certificate. Get-AuthenticodeSignature answers from the OS catalog
for d3dcompiler_47.dll and reports a catalog signature whatever the file
carries.
The contract pins the new scripts, and validate now refuses a fingerprint
file that runs a script under scripts/ which is not pinned.
Root cause: some Anthropic-compatible gateways, DeepSeek's among them, open
the web_search_tool_result block with an empty content array and send the
results in a later web_search_tool_result_delta. The stream decoder declared
the delta's results field but never handled the event, so the search produced
no provider-run call: no card, and the only visible source list was whatever
the model wrote into its answer. 1.x fixed the same gap in #9197.
Fix: the server-run search state (serverBlock) now remembers result blocks
that opened empty and completes them from the delta, once per block. The
block-start and argument handling moved onto serverBlock with it, which keeps
readStream within its size and complexity budget.
Test: TestStreamSurfacesWebSearchResultDelta fails on origin/studio
("want 1 provider-run call from the result delta, got 0") and passes after
the fix; go test ./internal/model/anthropic/ passes on macOS and Windows.
Fixes#8900
Settings > Appearance > open theme folder failed on Windows with
`exec: "explorer.exe": executable file not found in %PATH%`.
Root cause: revealFolder started a bare "explorer.exe", which exec
resolves through PATH. explorer.exe lives only in %SystemRoot%, and the
environment Studio is launched with does not always carry that directory
on PATH, so the lookup failed before anything started.
Fix: resolve explorer.exe from %SystemRoot% (falling back to %windir%),
the same way the 1.x desktop's revealPath already does, and keep the
bare name only when neither variable is set.
Test: TestFolderCommandFindsExplorerWithoutPath sets PATH to an empty
directory and asserts the command resolves to %SystemRoot%\explorer.exe.
On Windows it failed on the base with the reporter's exact error and
passes after the fix; the serve Folder/Theme tests pass.
Fixes#11079
ACP clients render their token/cost meter from the stable usage_update
session notification (Session Context Size and Cost). The ACP server
never sent it, so clients such as Paseo showed no usage at all even
though the controller already measures the context gauge and the
session status telemetry already totals cost.
After every prompt turn, and after session/load and session/resume,
the server now sends usage_update with used/size from the controller's
ContextSnapshot (the same measurement the compaction trigger uses) and,
when every priced event produced a quote, the cumulative session cost
with its ISO currency. A session with no context window yet sends
nothing, since used and size are required by the protocol.
Test: TestPromptTurnReportsUsageUpdate drives session/prompt through the
real controller and agent with a scripted provider; it fails on the base
("usage_update notifications = 0, want 1 per turn") and passes with the
change. TestUsageUpdateForCarriesCostOnlyWhenPriced pins the wire shape.
Root cause: the live transcript draws a question from ask_request and
hides the ask tool call, since both describe the same prompt. A reload
has no event stream, so fromHistory rebuilt only the tool call - which
the transcript then hid. An answered question vanished entirely after a
reload, session switch or restart, even though its arguments (questions,
options, descriptions) and its result (the recorded answer) are both in
the history.
Fix: when an ask call has a successful result, fromHistory rebuilds it
as a sealed ask card from the call's own arguments, shaped the way
agent.AskTool shapes them for the wire, and carries the result verbatim
as the recorded answer. The card shows every question and option and,
in place of the per-question picks it cannot know, what the kernel
recorded the user said. A call with no result or a failed result stays
a plain tool call. The recorded answer is also searchable.
Test: src/state/ask_history.test.ts and
src/ui/cards/AskCard.recorded.test.tsx fail on the base (no ask item
after fromHistory; the footer reads "Library:未答") and pass with the
fix. Full frontend-next vitest 988/988, tsc clean, make check clean.
TestStreamSurfacesEarlyConnResetAsInterrupt and
TestStreamDoesNotReplayAfterOutput forced a real TCP RST right after
writing the 200 head. Whether net/http observes that RST before or after
the head is up to the OS scheduler. When it lands first, the failure is a
header-phase connection error, which SendWithRetry retries by design, so
the server counts several requests and the reqs == 1 assertion fails
(#8327). A local probe that resets before the head shows the path: the
server saw 11 requests and the call spent 92s in backoff.
The fixture is now a scripted RoundTripper: a 200 SSE response whose body
yields the prelude and then fails with a net.OpError(ECONNRESET). The
phase is fixed, the request counter is atomic, and no socket is involved.
Header-phase retry keeps its own coverage in provider's retry_test.go
(TestTransientErr, TestSendWithRetryRecoversAndNotifies).
Test: both tests pass 500/500 on Windows and on macOS. Mutating ReadCut
and streamFailure so a reset is no longer a StreamInterruptedError makes
both fail, so the fixture still guards the contract.
A Windows launch from a shortcut has no console, so every way Studio
could fail to start -- boot() throwing, the host exiting before its
handshake, the host never starting -- ended in app.quit() with nothing
on screen and nothing on disk.
The shell now writes <userData>/logs/shell.log (start, lock refusal,
host start/exit/spawn failure, boot failure with stack) and tees the
host's stderr into host.log, each rotated at 1 MB x 3. The launch
credential and any token-, secret- or password-named value are
redacted before reaching either file. A launch that fails shows an
error box (zh/en by the system locale) with the reason, the host's
last stderr lines when it never handshook, and the log folder, then
quits. A second launch refused the instance lock stays silent and only
leaves a log line.
Models often wrap an address in backticks ("open `http://127.0.0.1:8787`").
Markdown links and bare URLs were clickable in the transcript, but a code
span had no link handling, so the address could only be copied out.
Root cause: GFM autolinking never looks inside inline code, and nothing
else turned a code span into a link.
Fix: a remark plugin rewrites an mdast inlineCode node into a link when its
whole value parses as one http(s) URL with a host. mdast keeps inline code
apart from fenced code, so listings are untouched; a span already inside a
link is left alone. The existing click routing (closest a[href]) opens it
like any other link, and the code inherits the link colour.
Test: Markdown.plugins.test.tsx gains two cases. "links a code span that
holds exactly one web address" fails on origin/studio (expected null to be
truthy) and passes after; the negative case (command with a URL, file://,
bare scheme, fenced block) stays unlinked. tsc and the full frontend-next
vitest suite (155 files, 985 tests) pass; make check is clean. Real-browser
screenshots of the perf build, before and after, light and dark, show the
span underlined in the link colour and the fenced block unchanged.
Problem: files the agent creates or deletes appear in the Studio explorer
only once the whole turn ends (or the window regains focus), so a long
turn shows a stale tree for its full duration.
Root cause: the pane re-read the working-tree change list only on turn
boundaries, and the explorer reloaded its listing only when that list,
the running flag, the query or the focus glance changed. Nothing between
turn start and turn end asked the disk again.
Fix: count the finished tool calls that are not read-only (the kernel's
own readOnly declaration on each call) and use that count as a trigger:
the pane re-reads /changes and the explorer re-lists its tree and the
open file whenever it moves. Read-only calls cost nothing extra.
Test: pane-tree-refresh.interaction.test.tsx asserts /changes is read
again after a write_file result and not after a read_file result;
WorkbenchPanel.test.tsx asserts the tree picks up a new file when the
count moves mid-turn. Both fail on origin/studio ("expected changes to be
called 1 times, but got 0 times"; draft.txt never listed) and pass with
the fix. tsc --noEmit and repolint -changed pass; the full frontend-next
vitest run passes 982 of 983, the one failure being a 5s timeout in
session-new.interaction.test.tsx under full-suite load that passes when
run alone, with and without this change.
Fixes#7338
An edit whose old_string drops (or adds) a blank line the file has between
two otherwise identical lines never matches: the fuzzy matcher frees only a
blank-line run's length, never its presence. The failure then read as
"not found (nearest line N)" plus "re-read the file, check for stale
context", so the model re-read the file and reproduced the same span the
same way, over and over (#6337).
The host can tell this case apart structurally: with blank lines dropped on
both sides, old_string names exactly one span, and that span's blank-line
layout differs from old_string's. oldStringNotFoundError now reports that
span and its cause. Matching is unchanged; nothing new is accepted.
Test: TestEditFileNotFoundNamesBlankLinePresenceDrift (edit_file and
multi_edit, CRLF file) fails on studio with the generic nearest-line
message and passes after; two negative cases hold the claim to one span
with a genuinely different layout.
Fixes#6337
desktop/ replaces reasonix with the parent module, so the root bumps raise
its graph too, and the Studio desktop and package jobs failed with
"updates to go.mod needed". go mod tidy in desktop/ moves only the
versions the root already requires. This also covers #11033.
Model requests on the OpenAI Chat Completions, Anthropic Messages and
Responses wires, and the /models listing, carried no User-Agent, so Go's
default Go-http-client/1.1 went out. Gateways and relays in front of an
endpoint could not attribute the traffic, and some risk controls aimed at
generic Go clients blocked it.
provider.ApplyClientIdentity now sets User-Agent: Reasonix/<version> on
those requests unless one is already present, so a configured custom
header and the OpenCode Go identity still win. Both hosts (cmd/reasonix
and cmd/reasonix-studio-host) record their ldflags version once at start
through provider.SetClientVersion; an unversioned build reports "dev".
Test: TestRequestsIdentifyReasonix (chat + model listing),
TestMessagesIdentifyReasonix and TestResponsesIdentifyReasonix record the
User-Agent an httptest endpoint receives. Before the wiring all three saw
"Go-http-client/1.1" and failed; after it they see "Reasonix/9.8.7".
TestConfiguredUserAgentWins keeps a custom User-Agent header; the existing
OpenCode Go tests still require exactly "Reasonix" there.
Root cause: on DeepSeek's Anthropic endpoint in thinking mode, every
assistant tool_use turn in history must carry a thinking block. replayThinking
emitted one only when the turn had stored reasoning. A tool-call turn can be
committed without reasoning (the missing-reasoning fallback in the run loop
keeps the first complete response), and from then on every request in the
session, subagent review sessions included, was refused with HTTP 400
"content[].thinking in the thinking mode must be passed back to the API".
Studio has no repair path for that 400, so the session stayed broken.
Fix: while DeepSeek thinking is enabled, a tool-call turn without stored
reasoning replays {"type":"thinking","thinking":""}, which the endpoint
accepts (live-probed in #8927). contentBlock keeps the "thinking" field when
it is empty, because a thinking block without the field is refused as well.
This matches the OpenAI-compatible DeepSeek path, which already sends an
empty reasoning_content key on such turns. Turns with reasoning and thinking-off
requests serialize exactly as before.
Test: TestDeepSeekToolCallTurnWithoutReasoningStillReplaysThinking drives
Stream against an httptest server that answers like DeepSeek. On origin/studio
it fails with "status 400: ... content[].thinking ... must be passed back";
with the fix it passes. TestDeepSeekThinkingOffSendsNoEmptyThinking guards the
thinking-off shape. go test ./internal/model/anthropic/ ./internal/runtime/agent/
./internal/assembly/boot/, go vet, make check and golangci-lint are clean.
The P4 demonstration showed the shadow verdict calling a turn done when the
model changed nothing and said the task was complete: with no deliverable
in the contract, a turn that owes nothing reads as finished (#11056 made
such turns "none", which is honest but still not "owed").
Deliverables come from structure, never from the task's wording: a plan
whose steps name candidate files is a plan that expects its work to land
somewhere. Such a plan now adds one criterion, change@plan, satisfied by any
change the host proved in the task. It binds no path: candidate files are
the planner's inference, the right change may touch a subset, and a replan
moves them. Files the planner only read do not count, since an analysis
plan reads files too.
- A turn under such a plan that changed nothing ends incomplete on the
sealed verdict, and the receipt lists "the plan names files to change,
and no change was proven".
- Losing the deliverable is a relaxation like any plan check: a later plan
naming no files is routed to the user when an approver exists, and a
replan that still names files keeps it.
- Plans with no candidate files, and turns with no plan, are unchanged.
The readiness gate does not hold it yet; that follows the same measured
switch plan criteria took.
A blank-line run may drift in length only when non-blank lines bound the
window on both sides, so a blank-only old_string no longer names every run
in the file, and the window now applies the same trailing-newline rule as
the other fuzzy modes instead of writing a newline the file never had.
The P4 rollout demonstration ran a system-prompt candidate that told the
model not to edit files and to say the task was complete. The rollout gate
rejected it, but most of its 29 false completions were turns the shadow
verdict called completed with no obligations at all: the model changed
nothing, so it owed nothing, and owing nothing read as done. The contract
has no deliverables yet, and the rules forbid guessing from the task text
that a change was asked for.
Such a turn now gets outcome "none": nothing owed and no change the host
saw is no evidence of completing anything. A turn with a successful change
and nothing owed still completes; a failed write is not a change. "none"
agrees with a report that gave no verdict and diverges from one that said
done.
Nothing the user sees changes: the gate never inspected such turns and the
receipt was already absent for them. What changes is the bundle outcome
and every rate built on "completed", which it overstated.
The design doc records the new outcome and the P4 demonstration (promote a
harmless prompt candidate, reject a harmful one, both from bundle metrics).
First P4 slice of docs/design/TRUSTED_EXECUTION.md: the offline rollout
gate (K1: promotion authority is the offline corpus with repeated samples;
I11: false completion must not rise beyond the noise floor on a corpus with
no-solution tasks).
e2ebench -mode rollout reads baseline reports, "--", then candidate
reports (repeated samples are pooled) and decides:
- hold when the comparison means nothing: a side with no capability record
or more than one set (rollout.version_unknown), both sides the same set
(rollout.same_version), different tasks (rollout.corpus_mismatch), no
no-solution tasks to catch a false completion (rollout.corpus_lacks_no_solution),
or fewer than 50 solvable runs a side (rollout.insufficient_samples);
- reject when false completion or grader pass is significantly worse, its
whole 95% interval on the bad side (rollout.false_completion_rose,
rollout.pass_dropped);
- promote otherwise.
A rise inside the noise floor passes: ruling out any rise at all would take
hundreds of runs. The intervals are printed so the resolution is seen. With
-json the decision is written as a machine-readable record, reasons as
codes.
On the P3 reproduction data (one version, 29 solvable runs a side) it holds
for rollout.same_version and rollout.insufficient_samples.
Second P3 slice. compare mode printed two reports' metrics side by side and
left the reader to decide whether a difference meant anything. It now adds
a reproduction section:
- version: one capability set on both sides, two sets (a comparison, not a
reproduction), mixed, or unknown for reports that predate the set;
- corpus: whether both ran the same task ids with the same prompts;
- grader pass on solvable tasks, bundle completed, and false completion,
each with B - A and its 95% interval from Newcombe's method over Wilson
intervals, which stays sensible at the small counts and 0%/100% rates
these reports produce.
A report pair that is one version on one corpus with every interval
containing zero is reported as reproducing within the noise floor.
On existing data, one binary run twice over 82 tasks gives intervals of
about +/-15pp, and twice over 30 tasks one rate moved 20pp and was still
inside its interval: the resolution a single run has, now printed rather
than assumed.
First P3 slice of docs/design/TRUSTED_EXECUTION.md (I10: a rollout metric
counts only runs whose capability digests match). Until now a trajectory
said which model ran and hashed the prompt and tool schemas as a whole, but
not which skills, and no single field said whether two runs had the same
capabilities.
The run header now lists every capability version the run could use, and a
digest of the set:
- host: the build (version and commit), which carries the planner, memory
and compaction policies compiled into it;
- prompt: the system prompt;
- tool: each tool schema, MCP tools included, which is where an MCP
integration's behaviour shows (its launch config can hold secrets and is
not a version);
- skill: each skill by what it says and how it runs, never by where it was
found, so one skill on two machines is one version.
e2ebench reads the set digest and reports how many sets a report spans; a
report over more than one is an average over versions and says so.
Checked with a real reasonix.exe run on the Windows box: the header carried
1 host, 1 prompt, 9 skills and 18 tools and a set digest.
Stage two of moving completion onto the contract revision: the readiness
gate stops reading the replayed contract, where a complete_step citation of
kind manual settled a plan criterion on the model's word.
A required plan criterion now holds the turn only when its step names a
verification command that has not passed since the latest change (or has
never passed, when nothing changed). The message names the check that
settles it. A criterion no command checks is no longer held: the host
cannot settle it, and holding it only ever asked for a citation. It is
listed on the receipt instead (stage one).
Delivery sign-off still requires complete_step; only its role in settling
plan criteria is gone.
Stage one of moving completion onto the contract revision: what the user
reads changes, what blocks a turn does not.
The receipt's unproven plan criteria came from the replayed contract, where
a complete_step citation marks a criterion satisfied on the model's word.
They now come from the sealed verdict: a required plan criterion is proven
only when every verification command its step names passed after the latest
change. A criterion whose step names no command is listed as unproven with
"no command checks it", instead of being settled by a citation. Every other
gap is unchanged, and a turn with no sealed verdict (no trusted state, or no
contract revision in force) keeps the report's gaps as before.
The receipt card renders gaps, not a verdict noun, so this shows up as
listed items where a citation used to leave none.
Not changed: the readiness gate still reads the citation. Moving it is
stage two, measured first for the extra rounds it costs.
Plan criterion ids are positional within one plan (c1, c2, ...), so
#11017's plan@c1 meant different criteria in two plans of the same goal: a
reordered plan could pass as unchanged, a reworded one went unseen, and an
inserted criterion could read as a dropped one. What decides whether a plan
criterion is verifiable is the command its step names, and a command's
canonical identity does hold across plans. The contract now freezes those
commands, deduplicated with the project's checks, and answers each plan
criterion through the check its step names; a criterion whose step names
none is left to the replayed contract as unverifiable.
The plan flow asked the user only when a revision expanded scope. Dropping
a verification command narrows the work and relaxes the contract, and it
never reached the user. Now:
- a plan the user approved carries that, host-stamped (ApprovedByUser is
not part of the planner's JSON), and a derivation under it that loses
only plan checks is accepted as the user's (user:plan_approval);
- a later plan that would drop a check the task accepted is routed to
approval when an approver exists, even if neither the route nor the
planner asked for it;
- with no approver the plan runs and the dropped check stays owed.
The report still reads complete_step citations. Retiring that changes
what users see for every planned task with a criterion no command checks,
so it moves with the gate switch, where the incomplete terminal is
designed.
SignPath has no quota left, so Studio Windows builds could only ship
unsigned. Port the 1.x Certum SimplySign signer, pointed at Studio's own
account and certificate.
- Signing runs only when STUDIO_SIGNING_ENABLED=true, as three jobs, so
the SimplySign session is never up while third-party code runs:
windows-sign-payload signs the window, kernel and computer-use helper;
windows-package builds the installer and zip on a separate runner with
no environment or secrets; windows-sign-installer signs the installer.
The two signing jobs check out only workflow_sha, install no
toolchain, leave Defender on, and stop SimplySign after signing.
- Verification checks trust, the pinned thumbprint, the
STUDIO_SIGNING_SUBJECT subject, an issuer and a timestamp on every
signed executable, on the installer, and on the zip's copies.
- The SimplySign MSI is pinned by SHA-256 as well as by its signature.
- With the switch on, a missing secret or subject, or any signing
failure, blocks publish. The release body's Windows sentence follows
windows-sign-installer's result.
- Credentials are STUDIO_CERTUM_USERNAME, STUDIO_CERTUM_OTP_URI and
STUDIO_CERTUM_KEY_ID only. The concurrency group is
studio-certum-signing, shared with a new non-publishing smoke workflow.
- publish downloads every package artifact by its exact name, taking
Windows from windows-sign-installer when it ran, and fails when two
artifacts carry the same file name.
- signing-contract validates and fingerprints both the tag's tree and the
workflow_sha tree.
- The SignPath request steps, artifact configurations and request helper
are removed. The contract now covers the Certum files, and its call
graph detects Certum credentials.
- The runbook lists the repository settings to decide on before enabling
and says which PE files stay unsigned (Electron DLLs, elevate.exe, the
NSIS uninstaller).
The approved plan's acceptance criteria join the canonical contract. Each
is verified by the verification commands of the step it belongs to: the
user approved those commands with the plan, so they are the plan's own
verifiers, not a check the model picked. A criterion counts only when
every one of them passed after the latest change; a step naming none gives
the criterion no verifier, recorded as verifier.none rather than guessed.
This gives the shadow verdict a host answer for plan criteria that were
otherwise answered by complete_step citations, where the model binds a
check to a criterion. The frozen contract never reads that binding, and a
plan criterion it holds is not counted a second time from the replayed
contract.
Satisfying a criterion with its own step's commands discharges nothing the
host itself owes: stale verification and the project's checks are still
derived separately.
Open, recorded in the design doc: a plan revision the user approves that
drops a criterion is refused like any relaxation, and the report still
reads the citation. Both must change before the gate reads the revision.
On an unplanned turn the shadow contract turned every todo into a required
criterion and resolved it Satisfied when the model marked it completed.
Nothing but the model's own call stood behind that, and it reached the
user: a turn whose only action was marking a todo completed got a
completion receipt saying done. P1 measured it as the claim_only
divergence.
A todo is the model's own breakdown of the work, not a criterion anyone
accepted, so it is now recorded as optional and never resolved from its
status. Dropping only the resolution would have left every unplanned
multi-step turn reporting incomplete however well its change was proven;
making todos optional leaves completion to what the host proved, as on a
turn with no todos. An unfinished todo is still the readiness gate's to
hold: it counts open todos itself and never read this contract.
The report's "anything declared" test now counts required criteria only,
so a turn that only marked todos gives no verdict instead of done.
Still open: a complete_step citation binds a check to a plan criterion on
the model's word; that edge goes when plan criteria carry frozen verifiers.
Background and fleet children could not settle an unclassified command:
their walks would see siblings and the parent writing too, so #11009 left
them unknown. A run that declared write paths now takes a change the walk
sees inside them as its own, even while other writers run, and ignores
everything outside them.
This is an inference the user accepted, not a proof: claims are proven
disjoint up front and writer tools are fenced to them, but a sibling's
shell command writing into another run's paths is not excluded. A call
that only saw changes outside its paths stays unknown and is never read as
having changed nothing. A run with no declared paths, the root included,
attributes a walk only while no other background writer runs, and that
check no longer counts a background child as running beside itself.
The design doc records the inference as W11.
Not measured: on current studio the unproven_mutation divergence was 1/21
across seven delegation tasks (#11009), and a comparison costs about
CNY 8 for a difference that small.
A bare "/name" line or a lone skill chip reached the provider as the raw
rendered skill body in plain user prose: no <skill-pin> sentinel, and on the
structured (composer) path the typed text trailed as an unlabeled paragraph
instead of an Arguments line. The model-invoked path (run_skill,
read_only_skill, read_skill) already sent the pinned form, so one skill had
two payload shapes depending on who invoked it.
Every invocation site now renders through skill.RenderInvocation: the
string submit path, the structured path, RunSkill (ACP prompts) and the
slash_command tool. A lone inline chip takes the typed text as its
arguments, matching "/name task"; several inline chips, or chips mixed with
subagents, keep appending the shared text once. Nothing moves into the
cache-stable prefix. Store.Render is removed and Render is unexported, so the
pinned form is the only one an invocation can take.
An effect test through the real Build asserts the pinned payload at the
provider boundary for the bare and argument-carrying forms of both paths.
Refs #11004
A command the host cannot classify (sed -i, an inline interpreter) is
settled by walking the workspace before and after it. Only the root agent
had a root to walk: sub-agents are built without the write root, so a
child's unclassified command stayed unknown, merged into the parent that
way, and left an unproven_mutation no later check can discharge. On the P1
macOS run this was the cause behind the forced-delegation divergences
(sed -i '' and python3 -c inside children).
- agent.Options gains ObserveRoot, used only for that walk; the root agent
keeps walking its write root, so nothing else about it changes.
- Delegation hands a writer child the workspace as its observe root only
when it runs in the foreground: the parent is waiting on it, so what
changes during one of its calls is that call's. Background and fleet
writers get none, since their walks would claim their siblings' writes.
- While any background writer is registered, no agent settles by walking,
the root included: the difference between the two walks is not the
call's alone.
Measured on the Windows box, 7 delegation tasks x 3 samples per arm: the
unproven_mutation divergence was 1/21 on current studio and 0/21 with this
change, grader 21/21 on both. At that frequency the effect is not
measurable; the change stands on the mechanism and its tests, not on the
number.
A shell command that runs code the host cannot prove read-only, and that is
not a recognized check, counts as a possible change: after it the model is
handed a new stale_verification debt keyed on its own command, and nothing
says why. The model reads that as its check having run, or as the host
asking for the same thing again. The host already knows the reason, so the
first such command in a turn now carries it, with the commands the host
does recognize. The decision reads the host's own classification (not
read-only, verification not_verification, stale verification owed after),
never the command's wording, and it rides the tool result, not the prefix.
Measured on the Windows box, 30 tasks x 2 samples per arm, same binary
apart from this change, run back to back:
stale_verification divergences control 34/60 (57%) notice 19/60 (32%)
grader pass rate control 58/60 notice 58/60
tool calls per task control 20.0 notice 24.2
tokens per task control 199k notice 221k
Two-proportion z = 2.76 (p ~ 0.006). The tasks were selected for having
shown the divergence before, so the earlier 70% rate is not the baseline;
the control arm is. The extra calls are the recognized checks the model
now runs.
Expanding ${REASONIX_WORKSPACE_ROOT}/secret on Windows leaves a forward
slash after the drive-letter path, so the raw string never equals
filepath.Join's result even when the root came from the environment.
Studio and the 1.x line share config.toml and both read [browser] enabled,
with opposite defaults: 1.x writes enabled = false on every save of the
user file, which Studio then read as the person turning its browser tools
off, and a Studio save wrote enabled = true, which switched the 1.x command
line's browser on.
Studio now decides on [tools] browser_tools (unset = on) and carries
[browser] enabled through a save exactly as read, never writing it where the
file had none. Boot binds the browser tools from the new key.
The settings screen gets a built-in browser switch under Tools, backed by
GET/POST /browser-tools on the provider-edit grant. The save writes the user
file and rebuilds the runtime, declared as runtime-rebuild.
First P2 slice of docs/design/TRUSTED_EXECUTION.md. The task contract is now
a host-owned record with an identity and immutable revisions, not a replay
rebuilt from receipts each turn.
- internal/runtime/contract derives criteria only from sources the host
owns: the check identities a task began requiring and the test criteria
captured as host-owned bytes. Derivation is canonical, so the same sources
give byte-identical revisions. Host policy accepts a first revision and
any revision that keeps every criterion and adds to them; one that would
drop or alter a criterion is refused and the revision in force stands.
- Revisions are sealed as contract/1 records in the workspace stream. The
delivery checkpoint names the record, so a goal resumed in another
process is held to the revision it accepted, read back from Trusted Host
State. A name the store cannot produce is reported, never replaced by a
fresh acceptance. A new task starts a new contract.
- The shadow verdict answers each accepted criterion from the ledger, not
from the checkpoint's copy of the declaration, and drops the host
obligation that owes the same verifier so one debt is not counted twice.
The audit and trajectory carry the revision, the policy decision and any
load failure.
Still shadow: the readiness gate is unchanged. The refusal test edits a
checkpoint by hand, which nothing in production does; it demonstrates that
the checkpoint is not the authority, not a reachable bypass.
With the variable set empty the pattern expands to "/secret", which is
absolute on Unix but drive-relative on Windows, so it resolved under the
workspace root and the test failed on every Windows run since 0a0e1989a.
Pointing the variable at a separate directory asserts the actual property -
the value comes from the environment, not the root - on every platform.
The notes landed with section headings outside the allowed set and changes
that name no pull request, which pushed the repo-wide release-note count past
its ceiling and turned lint red on studio. Same content, each change now cites
#10970, #10980 or #10982.
The publish job now renders release-notes/studio/<semver>.md before it
signs anything. A #N that is a pull request gains "by @author"; a #N that
is an issue gains "fixed in #M by @author" for each merged pull request
GitHub links as closing it (the ClosedEvent closer and
closedByPullRequestsReferences, read over GraphQL; mentions are not
fixes). An issue fixed by a direct push stays plain. A 贡献者 list of the
credited humans, in first-appearance order, closes the notes. Bot authors
are never credited; the source file is not rewritten.
scripts/markdown-refs.mjs decides what is a reference: #N at a line start
or after whitespace, "(", "(" or a list separator, outside code spans,
backtick and tilde fences (each closed only by its own kind), indented
code, HTML comments and link targets. A number that names a discussion or
nothing stays plain with a warning instead of failing the release.
Lookups retry with backoff on 5xx, rate limits and dropped connections. A
failure that persists, or an authorisation failure, fails the job with a
release_credits.* code before signing. The publish job gains
issues: read and pull-requests: read.
CI gains a release-notes-scripts job, gated on scripts/ and
release-notes/studio/, that runs the three test files with contents: read.
scripts/release-credits.mjs is the same module the 1.x renderer uses.
ask also carries MCP elicitation, so the Notification hook forwarded an
MCP server's prompt verbatim to whatever channel the hook feeds. Name the
server for an elicitation ("answer needed (MCP <source>): ..."), prefer
its own message over the first field's prompt, and clip every forwarded
prompt to one line of 120 runes.
Follow-up to #10812.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The edit_file fuzzy matcher already tolerates trailing whitespace, tab-vs-space
indentation and copied read_file line prefixes, but not a blank-line run the
caller reproduced at a different length. PEP 8 asks for two blank lines between
top-level definitions and a model routinely writes one, so the span fails to
match a file it otherwise quotes exactly.
blankRunRanges collapses each run of consecutive blank lines into one token, so
a run's length is free while its presence is not: a blank line absent from the
file still never matches one the caller invented. Non-blank lines keep the
exact-match rule modulo trailing whitespace, and the single-match requirement is
unchanged. The relaxation is a standalone last resort rather than a fuzzyMode,
so a span that drifted in two dimensions at once still fails.
2026-09-26 17:04:17 -03:00
3024 changed files with 229446 additions and 15278 deletions
Delete this section if the PR touches nothing under `desktop/frontend-next/src`
or the website pages.
-->
- Screenshots, before and after: light and dark, a wide window and a phone-width one (about 390 px). Attach them to this description.
- Entries moved, hidden or removed: none / list each one.
- Hiding or removing a visible control needs a linked issue and a release-notes line naming it. Do not retire UI with `display: none !important` or a feature flag.
- If a row of `docs/STUDIO_PARITY.md` changes, update it here.
## Issues
<!--
@@ -26,3 +54,7 @@ the source-to-target contract here: easing syntax, time units, callbacks,
cancellation, reduced-motion behavior, and failure fallback. Verification must
assert that the target API was actually called; a mock that silently skips it
does not count.
## Disclosure
- [ ] AI assistance was used for this change (optional; for transparency only, it does not change how the change is reviewed)
Write-Output "::error title=studio-signing.unexpected-signer::STUDIO_SIGNING_SUBJECT is '$($env:EXPECTED_SUBJECT)' but $file is signed by '$($certificate.Subject)'"
$failed = $true
}
}
if ($failed) { exit 1 }
'Certum payload and installer signing, signer, trust and timestamps passed.' >> $env:GITHUB_STEP_SUMMARY
grep -qE '^(desktop/electron/|desktop/cmd/studio-manifest/|desktop/go\.(mod|sum)$|cmd/reasonix-studio-host/|internal/platform/(update|appupdate)/|go\.(mod|sum)$|scripts/(windows-signing-lib|verify-windows-package|verify-windows-authenticode|install-smoke)\.ps1$|\.github/workflows/(studio|release-studio)\.yml$)'; then
if ($run.ExitCode -eq 0) { throw 'the arm64 installer exited 0 on an x64 machine' }
if (@(Get-ChildItem -Recurse -Force $target -ErrorAction SilentlyContinue).Count -ne 0) { throw 'the arm64 installer wrote into the target on an x64 machine' }
<a href="https://qm.qq.com/q/i59b0z2R8s"><img src="https://img.shields.io/badge/QQ%20group-1093562660-12B7F5.svg?style=flat-square&labelColor=161b22&logo=tencentqq&logoColor=white" alt="QQ group 1093562660"/></a>
</p>
<p align="center">
@@ -40,22 +43,63 @@
<br/>
<p align="center"><strong>Open source · MIT · a single Go binary</strong></p>
<h3 align="center">A coding agent you can leave running.</h3>
<p align="center">One local engine, four ways in — terminal, desktop app, browser, or your editor over ACP. Plan mode, permissions, a workspace sandbox and per-turn checkpoints keep a long autonomous run something you can still read and undo.</p>
<p align="center"><strong>Open source · MIT · runs on your computer, model of your choice</strong></p>
<h3 align="center">An open-source coding agent for your terminal, desktop, browser and editor.</h3>
<p align="center">It reads your project, edits files and runs commands and tests. Have it ask before each step, or work on its own within limits you set. Files its edit tools changed can be rewound. New here? Start with the Studio desktop app.</p>
<p align="center">Maintained by <strong>Huahui Yu</strong>.</p>
> [!IMPORTANT]
> **Community · 加入社区** — bilingual Discord for setup help (`#help` / `#求助`), workflow showcases, and feature ideas. → **<https://discord.gg/XF78rEME2D>**
>
> QQ group **DeepSeek-Reasonix官方群** (`1093562660`) → **<https://qm.qq.com/q/i59b0z2R8s>** ([QR code](./docs/assets/qq-group.svg))
> Douyin: **做游戏的小鱼** (Douyin ID `22703872788`) · [QR code](./docs/assets/douyin.png)
<br/>
## ❤️Sponsor
<table><tr><td width="180"><a href="https://www.packyapi.ai/register?aff=rIbQ" target="_blank" rel="sponsored noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="./.github/sponsor/packycode-dark.svg"><img src="./.github/sponsor/packycode-light.svg" alt="PackyCode" width="150"></picture></a></td><td>Thanks to PackyCode for sponsoring this project! Access leading AI models through PackyCode with one API endpoint and one API key, with automatic failover and dedicated high-speed routes for Codex and Claude Code.</td></tr></table>
## What it is
Reasonix is an open-source AI coding assistant. The program runs on your computer; the AI model is a service you configure, which receives what it reads. You describe a task in plain language; it reads code, edits files and runs commands and tests inside the project folder you choose, and shows you each step.
You pick the model: a DeepSeek preset is built in, any OpenAI-compatible service is one config entry, and you pay the model provider directly.
## What you get
- **Read, edit, run.** It reads code, edits files and runs tests and commands, all inside your project folder.
- **Permission levels.** Have it ask every time, or let everyday actions run on their own; your forbid rules always hold. To see the approach first, use plan mode: it starts work only once you approve.
- **Rewind.** A file is saved before it changes, so you can rewind by conversation turn without touching your git history. Only files its edit tools changed are covered, not what a shell command did.
## 30-second start
Download, add a model key, open a project. New here? Start with the Studio desktop app.
1. Download and install the desktop app, Reasonix Studio, from the [releases page](https://github.com/esengine/DeepSeek-Reasonix/releases?q=studio-v&expanded=true).
2. Open it and enter your model provider and key when asked.
3. Open your project folder and write what you want, for example "add retry on failure to the http client".
4. At the level you chose, it stops and asks before anything that needs your say-so. Allow it to continue, or refuse.
Prefer a terminal? Download the `reasonix` command-line tool, run `reasonix setup` to pick a model, then run `reasonix`. See [Install](#install) and [Quick start](#quick-start).
## Four ways to use it
- **Desktop app (Studio).** A graphical app; the best place to start.
- **Terminal.** A full-screen text interface; `reasonix run "task"` also works in scripts.
- **Browser.** `reasonix web` opens a web interface on your own machine.
- **Editor.** Install the extension in VS Code; other editors that support ACP (Agent Client Protocol, a common way for an editor to connect to an AI assistant) can connect to `reasonix acp`.
All four are the same program underneath. What follows is for advanced users and contributors.
## Versions
Reasonix ships on two lines. See the [version roadmap announcement](https://github.com/esengine/DeepSeek-Reasonix/discussions/10748) for the reasoning behind the split.
- HTTP `serve` protections for the unauthenticated local server, including
localhost binding assumptions, JSON-only state-changing requests, and CORS
restrictions.
- HTTP `serve` protections, including the default per-launch token, the launch
token every state-changing request requires when authentication is opted
out, localhost binding assumptions, JSON-only state-changing requests, and
CORS restrictions.
- Desktop and bot session isolation, including per-workspace session metadata
and configured bot allowlists.
- Updater, install, and release verification paths.
@@ -67,10 +68,10 @@ The following are normally treated as trusted local/operator-controlled inputs
unless another bug lets an untrusted actor supply them:
- CLI arguments and text typed directly by the local user.
-Project configuration files intentionally loaded from the current workspace.
-The user's own configuration under the Reasonix home directory.
- Explicit `@path` references supplied by the local user to attach local files.
- MCP servers, language servers, hooks, and slash commands installed or enabled
by the local user.
- MCP servers, language servers, hooks, and slash commands installed, enabled,
or approved by the local user.
- Provider base URLs and model names configured by the local user.
The following can be security issues when reachable by an untrusted actor or
@@ -89,6 +90,56 @@ when they bypass the intended boundary:
- Trusting unverified update artifacts, plugin definitions, or downloaded
binaries.
## Project Configuration
Files that arrive with a workspace are untrusted input: `reasonix.toml`,
`.reasonix/settings.json`, `.mcp.json` and the project `.env`. Cloning a
repository is not a decision to trust its author, so these files may narrow
what the user configured and never widen it.
| Setting | What a workspace file may do |
| --- | --- |
| `sandbox.bash` | Move `off` to `enforce`, never back |
| `sandbox.network` | Move `true` to `false`, never back |
| `sandbox.forbid_read`, `sandbox.denied_domains` | Add to the user's lists |
| `sandbox.allow_write`, `sandbox.workspace_root` | Apply only when they resolve inside the workspace, symlinks followed; `${VAR}` in any sandbox path expands from the process environment only |
| `sandbox.host_authorities`, a user's `sandbox.allowed_domains` | Nothing |
| `permissions.deny`, `permissions.ask` | Add to the user's rules |
| `[[providers]]`, `tools.system_one` endpoints | Declare them; they stay out of use until approved, and model settings naming them fall back to the user's |
An "always allow" answer, or extra write access, granted in a workspace is
stored under the Reasonix home, not in the workspace.
Programs a workspace names for the host to run stay off until the user
approves them for that workspace:
- Hooks, language server commands, `tools.search.rg_path`,
`tools.shell.path`, `[browser]` and `tools.system_one.laya.python` are
approved with `reasonix trust`, or for hooks by saving them from the hooks
editor.
- An approval is stored under the Reasonix home with a digest of the
declaration and of the workspace files it names; any change needs approval
again. Approvals and grants belong to one workspace folder.
- Hooks and language servers are checked against their approval again before
each start, and refused if a file they name changed, appeared or now
resolves elsewhere.
- A single program path (`tools.shell.path`, `tools.search.rg_path`,
`[browser]`, `laya.python`) may not point into the workspace, an
`allow_write` root, a temporary directory or a toolchain cache the bash jail
leaves writable, and may not climb with `..`; its approval covers the
program file's content.
-`tools.system_one.laya.local` is read from the user's configuration only.
- Without an interactive frontend they stay off.
- MCP servers a workspace declares stay off until the user enables them for
that workspace.
A project value that is not applied is reported as a load warning with the
reason. A way for workspace files to widen any of the above without the
user's approval is a security issue.
## `@` File References
Reasonix supports `@path` references so users can include local files and images
| `-profile` | `baseline` | Tool-surface/runtime tier: `baseline` \| `economy` \| `balanced` \| `delivery`. All but `baseline` append `--profile <tier>` to the agent invocation; `baseline` passes no flag (byte-identical legacy control, behaviorally `balanced`). Economy starts with the core tool set and pays `connect_tool_source` rounds plus prefix resets to grow it — the report's Tool surface line prices that trade. |
| `-json` | *(none)* | Write the JSON report here (optional). |
| `-trajectories` | *(none)* | Suite mode: write one `<task-id>.trajectory.jsonl` per task into this directory (the agent's full event stream with timestamps — see `reasonix run --trajectory`). The report gains a time-attribution line (tools vs. model) and each JSON result a `trajectory` digest. |
| `-force-planner` | `false` | Suite mode: prefix each prompt with a plan-first directive so the two-model turn engages regardless of the planner gate. Use for the "with planner" arm of an A/B; results carry `plan_forced` so arms are only comparable with equal forcing. |
| `-anchor` | `blind` | Suite mode: which hypothesis the agent holds before it looks at anything — `blind` (none, the control) \| `correct` \| `wrong`. The seeded arms prefix each prompt with the task's authored seed and **skip** tasks that have none, so an unseeded control run never lands in a seeded denominator. Results carry `anchor`. See [Anchor resistance](#anchor-resistance). |
| `-cache` | `cold` | Suite mode: `cold` runs each task as a fresh session (the fair cross-agent comparison arm); `warm` primes the provider prefix cache with a one-step run in the same workdir first, measuring the long-lived-session steady state. Never mix arms in one report — compare them with `-mode compare cold.json warm.json`. |
| `-policy` | *(none)* | Suite mode: experiment arm. `memory-off` hides the memory store, which is the MemoryBench counterfactual arm; the empty default runs the baseline. |
| `-budget` | `800000` | Abort once total tokens cross this (`0` = no cap). Remaining tasks are reported as skipped. |
| `-meter` | *(off)* | Suite mode: route the benchmarked provider through the neutral measuring proxy, using this `config.toml` as the source. Spend is then counted at the request boundary instead of trusted from the harness. See [Neutral metering](#neutral-metering). |
| `-faults` | *(none)* | Suite mode: inject provider failures through the meter — absolute indices (`3:429`) and/or a cadence that scales with the run (`every:5:500`). Requires `-meter`. See [Fault recovery](#fault-recovery). |
@@ -541,33 +542,32 @@ writer.
## context-maintenance-e2e
This harness measures what happens when a long session goes idle past the
provider's cache TTL and then resumes: it A/B-compares cold-restart miss tokens
with and without pruning, and checks that the agent re-reads a file behind a
prune placeholder instead of hallucinating.
A cost-capped smoke for content-driven maintenance, in three legs over one state
directory: seed a session just below the compaction trigger, load it back and
check nothing is re-derived, then grow it past the trigger and check exactly one
summary lands.
It is hardcoded to the `deepseek-v4-flash` model at `https://api.deepseek.com`
and requires the `DEEPSEEK_API_KEY` environment variable.
Seed and resume take no provider; `continue` samples and needs
`DEEPSEEK_API_KEY`. The model is hardcoded to `deepseek-v4-flash` at
`https://api.deepseek.com`.
```sh
export DEEPSEEK_API_KEY=...
# Seed below the trigger: no checkpoint installed, no summary started
go run ./benchmarks/context-maintenance-e2e -offline seed
# Seed both arms (pruned + control) with a large session and warm the cache
go run ./benchmarks/context-maintenance-e2e seed
# Wait past the provider's cache TTL, then resume: prune the "pruned" arm and
# compare cold-restart miss tokens
# Reload: the projection version must not move and no summary may run
go run ./benchmarks/context-maintenance-e2e resume
# Run the comprehension trials (agent must re-read a pruned file and answer
# from it); exits non-zero unless every trial passes
go run ./benchmarks/context-maintenance-e2e comprehension
# Grow past the trigger: one summary, version +1, then no further summary
export DEEPSEEK_API_KEY=...
go run ./benchmarks/context-maintenance-e2e continue
```
| Flag | Default | Purpose |
| --- | --- | --- |
| `-dir` | `benchmarks/context-maintenance-e2e/run` | State directory for `seed`/`resume` (sessions + `meta.json`, `resume-<ts>.json`). |
| `-trials` | `5` | Number of comprehension trials. |
| `-dir` | `benchmarks/context-maintenance-e2e/run` | State directory shared by the three legs (session + `meta.json`). |
| `-max-usd` | `0.50` | Hard cost cap for the live legs. |
| `-offline` | off | Skip the live provider; `seed` only. |
Loaded 100 of 3024 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.