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
1189 changed files with 70757 additions and 6448 deletions
if ([string]::IsNullOrWhiteSpace($env:CERTUM_USERNAME)) {
Refuse 'studio-signing.missing-username' 'Secret CERTUM_USERNAME is not set'
}
if ($env:CERTUM_OTP_URI -notmatch '^otpauth://totp/') {
Refuse 'studio-signing.invalid-otp-uri' 'Secret CERTUM_OTP_URI must be a complete otpauth://totp/ URI'
}
if ($env:CERTUM_KEY_ID -notmatch '^[0-9a-fA-F]{40}$') {
Refuse 'studio-signing.invalid-key-id' 'Secret CERTUM_KEY_ID must be the 40-character SHA-1 certificate thumbprint'
}
if ([string]::IsNullOrWhiteSpace($env:EXPECTED_SUBJECT)) {
Refuse 'studio-signing.subject-unset' 'Variable STUDIO_SIGNING_SUBJECT is empty; run the Studio Certum signing smoke test and copy the subject it reports'
if ([string]::IsNullOrWhiteSpace($env:CERTUM_USERNAME)) {
Refuse 'studio-signing.missing-username' 'Secret CERTUM_USERNAME is not set'
}
if ($env:CERTUM_OTP_URI -notmatch '^otpauth://totp/') {
Refuse 'studio-signing.invalid-otp-uri' 'Secret CERTUM_OTP_URI must be a complete otpauth://totp/ URI'
}
if ($env:CERTUM_KEY_ID -notmatch '^[0-9a-fA-F]{40}$') {
Refuse 'studio-signing.invalid-key-id' 'Secret CERTUM_KEY_ID must be the 40-character SHA-1 certificate thumbprint'
}
if ([string]::IsNullOrWhiteSpace($env:EXPECTED_SUBJECT)) {
Refuse 'studio-signing.subject-unset' 'Variable STUDIO_SIGNING_SUBJECT is empty; run the Studio Certum signing smoke test and copy the subject it reports'
echo "::error::environment studio-cli-channels has no required reviewers (or its protection rules were unreadable: '${reviewers}'); refusing to publish"
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
<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">Maintained by <strong>Huahui Yu</strong>.</p>
> [!IMPORTANT]
> **Community · 加入社区** — bilingual Discord for setup help (`#help` / `#求助`), workflow showcases, and feature ideas. → **<https://discord.gg/XF78rEME2D>**
- 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. |
printf '{"event":{"kind":"tool_result","tool":{"id":"read-1","name":"read_file","output":"A unique answer that the graded child must not read."}}}\n' >> "$2"
{"glob lists name only",`{"event":{"kind":"tool_result","tool":{"name":"glob","args":"{\"pattern\":\"/repo/tasks/demo/memory/project/*.md\"}","output":"/repo/tasks/demo/memory/project/fact.md"}}}`,false},
iferr:=os.WriteFile(answer,[]byte("---\nname: fact\n---\nA fact body in the first trajectory leg.\n"),0o600);err!=nil{
t.Fatal(err)
}
dir:=t.TempDir()
forindex,data:=range[]string{
`{"event":{"kind":"tool_result","tool":{"name":"bash","args":"{\"command\":\"cat /repo/tasks/demo/memory/project/fact.md\"}","output":"A fact body in the first trajectory leg."}}}`,
check("the picture is the guest page's own pixels",bmp[mid+2]>150&&bmp[mid+1]<100,[bmp[mid+2],bmp[mid+1],bmp[mid]]);
// A show that lands while a freeze is still taking its picture wins.
views.show(page.targetId,rect);
awaitwait(300);
constracing=views.freeze();
views.show(page.targetId,{...rect,width:601});
awaitracing;
check("a freeze overtaken by a show leaves the page shown",views.shown===page.targetId&&view.getBounds().x===100,{shown:views.shown,b:view.getBounds()});
views.hide();
check("with nothing shown, freeze answers with no picture",(awaitviews.freeze())==="","");
"留空时按这个顺序尝试:先 ssh-agent 持有的密钥,再 ~/.ssh 下的默认密钥;只有填了下面的环境变量或该机器要求交互时,才会走密码。":"Left blank, it tries in this order: keys held by ssh-agent, then the default keys under ~/.ssh. A password is only used when one of the variables below is set, or the machine asks interactively.",
"在代码编辑器中打开工作区":"Open the workspace in a code editor",
"在系统文件管理器中显示工作区":"Show the workspace in the system file manager",
"The agent can call them; every call still goes through permission checks",
"自上次有进展以来已用约 {n} 倍上下文的输入 token":"About {n}× the context window in input tokens since the last progress",
"已连续 {n} 轮没有可观察的进展":"No observable progress for {n} rounds",
"共 {n} tokens":"{n} tokens in all",
"没有文件改动、检查变化或新读取":"No file changed, no check moved, nothing new read",
"任务已暂停:{s}":"Task paused: {s}",
"继续(计数清零)":"Continue (resets the count)",
"不再提示(本会话)":"Don’t show again (this session)",
"任务已按设置暂停:自上次有进展以来的输入已达上下文窗口的 {n} 倍":"Paused by your setting: input since the last progress reached {n}× the context window",
"任务已按设置暂停:连续 {n} 轮没有可观察的进展":"Paused by your setting: {n} rounds in a row without observable progress",
"模型在重复输出同一段文字":"The model is repeating the same text",
"同一段内容被逐字重复":"The same block is being repeated verbatim",
"任务已按设置暂停:模型卡在重复输出同一段文字":"Paused by your setting: the model is stuck repeating the same text",
"等待你的输入":"Waiting for you",
"模型卡在重复输出同一段文字,这一轮已停止;可以重试、补充引导,或换一个供应商/模型":"The model is stuck repeating the same text; this turn was stopped. Try again, add guidance, or switch provider/model.",
// English for the cloud backup section under Settings > Account.
exportconstEN_BACKUP: Record<string,string>={
"云备份":"Cloud backup",
"把本机的模型、扩展、记忆等配置加密保存到账号,换机器或重装后按项恢复。":
"Encrypt this machine's models, extensions, memory and other setup into your account, and restore it item by item on a new machine or after a reinstall.",
"模型与界面设置":"Models and interface",
"技能、插件与 MCP":"Skills, plugins and MCP",
"长期记忆":"Long-term memory",
"Hooks 与状态栏命令":"Hooks and status line command",
"API 密钥":"API keys",
"供应商、默认模型、预设与界面外观;不含审批模式、权限和沙箱":
"Providers, default model, presets and appearance; approval mode, permissions and sandbox are never included",
"插件只记录来源与版本,恢复时仍走插件安装确认":
"Plugins are recorded by source and version only, and restoring one goes through the usual install confirmation",
"用户级指令文件与全局记忆":"User-level instruction files and global memory",
"会在本机运行的命令,恢复时逐项确认":"Commands that run on this machine; each one is confirmed on restore",
"默认不备份。勾选后密钥会随备份一起加密上传":"Off by default. When ticked, keys are encrypted and uploaded with the backup",
"备份在本机用你设置的口令加密后才上传,服务端看不到其中内容。忘记口令就无法恢复。":
"A backup is encrypted here with your passphrase before it is uploaded, so the service cannot read it. A forgotten passphrase cannot be recovered.",
"备份内容":"What to back up",
"备注(可选)":"Label (optional)",
"例如:公司笔记本":"e.g. work laptop",
"加密口令(至少 {n} 个字符)":"Passphrase (at least {n} characters)",
"再输一次":"Repeat it",
"两次输入的口令不一致":"The two passphrases differ",
"正在加密上传…":"Encrypting and uploading…",
"备份到账号":"Back up to account",
"已用 {n}/{max} 份":"{n} of {max} used",
"已备份。":"Backed up.",
"已备份。{n} 个文件过大或无法读取,未包含在内。":"Backed up. {n} files were too large or unreadable and were left out.",
"账号里还没有备份。":"No backups in this account yet.",
"未命名备份":"Untitled backup",
"恢复…":"Restore…",
"确认删除":"Confirm delete",
"正在读取备份…":"Loading backups…",
"本机没有":"Not on this machine",
"与本机不同":"Differs from this machine",
"与本机相同":"Same as this machine",
"需安装":"Needs install",
"我已看过这条命令,允许它在本机运行":"I have read this command and allow it to run on this machine",
"我确认允许把对话和本机保存的密钥发往这个地址":"I allow conversations and the key stored here to be sent to this address",
"我确认用备份里的密钥替换本机已保存的密钥":"I allow the key in the backup to replace the one stored here",
"我已读过这份指令文件,允许它引用其中列出的文件":"I have read this instruction file and allow the files it imports",
"已恢复 {n} 项。":"Restored {n} items.",
"配置已写入,但当前会话未能重新加载:{e}":"The setup was written, but this session could not reload it: {e}",
"以下插件需要走正常的安装确认:":"These plugins go through the usual install confirmation:",
"安装…":"Install…",
"输入备份「{name}」的加密口令,先预览它和本机的差异,不会立即写入。":
"Enter the passphrase for “{name}” to preview how it differs from this machine. Nothing is written yet.",
"加密口令":"Passphrase",
"正在解密…":"Decrypting…",
"预览差异":"Preview differences",
"这份备份来自 {p}":"This backup comes from {p}",
"命令和路径在另一种系统上可能无法运行,这类项目默认不勾选。":
"Commands and paths may not work on another system, so those items start unticked.",
"本机现为:":"Here now: ",
"本机不存在:{p}":"Missing here: {p}",
"来自另一种系统,可能无法运行":"From another system; it may not run here",
"备份时有 {n} 个文件过大或无法读取,未包含在内。":"{n} files were too large or unreadable at backup time and are not included.",
"正在恢复…":"Restoring…",
"恢复所选 {n} 项":"Restore {n} selected",
"还有 {n} 项需要逐项确认":"{n} items still need confirmation",
"Once trusted, edits and commands in this folder run without asking each time, here and in the terminal. The OS sandbox limits what commands write to this folder, the directories you allowed writes to, temp and toolchain caches; network and deny rules still apply.",
"No OS sandbox is in effect (Windows has none yet, or it is turned off in Settings), so nothing limits what commands write - edits and commands ask you first. Switching to auto-approve applies to new sessions in every folder.",
"未能导出该插件:{detail}":"That plugin could not be exported: {detail}",
"无法解析本次安装请求:{detail}":"That install request could not be read: {detail}",
"安装失败:{detail}":"It could not be installed: {detail}",
"这不是有效的社区包名称":"That is not a community package name",
"社区市场中没有这个已审核的包":"The community market has no approved package by that name",
"无法连接社区市场:{detail}":"The community market could not be reached: {detail}",
"社区市场返回了无法识别的内容":"The community market answered with something unrecognisable",
"社区市场暂不支持只列出已固定内容的包":"The community market cannot list only pinned packages yet",
"该包的审核版本没有固定内容,需要信任发布者后才能安装":"The reviewed version is not pinned to its content; trust the publisher to install it",
"该包的审核版本指向本机或不安全的来源,不从市场安装":"The reviewed version points at a local or unsafe source, which the market does not install",
"审核版本已更新,请重新查看后再安装":"A newer version has been approved - look at it again before installing",
"来源内容已与审核版本不同,已拒绝安装":"The source no longer holds the reviewed content, so nothing was installed",
"该来源的内容无法固定,已拒绝安装":"This source cannot be pinned by content, so nothing was installed",
"来源内容在确认后发生了变化,请重新查看":"The source changed after you confirmed - look at it again",
"这个标为主题的包还会安装主题以外的内容,已拒绝安装":"This package is listed as a theme but would install more than themes, so nothing was installed",
"登录账号后才能发布或评价":"Sign in to publish or vote",
"请先在 id.reasonix.io 验证账号邮箱,再发布或评价":"Verify your account email at id.reasonix.io before publishing or voting",
"这个名称已被其他发布者使用,请换一个名称":"That name belongs to another publisher - pick another one",
"这个版本号已经发布过,请换一个版本号":"That version is already published - use another version",
"请求太频繁,请稍等一分钟再试":"Too many requests - wait a minute and try again",
"这个来源审核通过后也无法从市场安装:插件和主题要指向固定到提交的 GitHub 目录(tree/40 位提交号),技能要用 https 地址,MCP 用 https 地址或 npm 包名":"The market could not install this source even once approved: plugins and themes must point at a GitHub directory pinned to a commit (tree/<40-character commit>), skills at an https address, MCP servers at an https address or an npm package name",
"社区市场拒绝了这次提交:{detail}":"The community market refused the submission: {detail}",
"你的发布里没有这个包":"None of your packages has that name",
"只有私有的包才能提交审核":"Only a private package can be submitted for review",
"未审核的版本要先预览,再按预览时的内容摘要安装":"Preview an unreviewed version first; it installs only against that preview's content digest",
"不能评价自己发布的包":"You cannot vote on a package you published",
"评价只能是赞、踩或撤回":"A vote is up, down or withdrawn",
"无法解析安装器的响应:{detail}":"The installer's own answer could not be read: {detail}",
"无法读取该主题:{detail}":"That theme could not be read: {detail}",
"这不是可安装的主题:{detail}":"That is not an installable theme: {detail}",
"A version change is already running — wait for it to finish and try again",
"本次版本切换未能启动:{detail}":"This version change did not start: {detail}",
"有 {n} 项任务正在运行,重启会中断它们":"{n} tasks are running, and a restart would stop them",
"没有已下载好的版本可供重启安装,请重新点击安装":"No downloaded version is waiting for a restart; choose Install again",
"本次启动未能记录为健康状态:{detail}":"This launch could not be recorded as healthy: {detail}",
"该请求未发往 Studio 监听的地址,已被拒绝":"That request was not addressed to the address Studio listens on — refused",
"该页面不属于 Studio,无法对其操作":"That page is not Studio itself, so it may not drive it",
"缺少本次启动的凭据,请重新打开 Studio 后重试":"This launch's credential is missing — reopen Studio and try again",
"本机通道未建立,请重新打开 Studio 后重试":"The local channel was never established — reopen Studio and try again",
"该服务未开启认证,只接受发往本机或其监听地址的请求":"This server runs without authentication, so it only accepts requests addressed to this machine or its listening address",
"该服务未开启认证,修改与审批需要本次启动的令牌,请用启动时打印的链接打开":"This server runs without authentication, so changes and approvals need this launch's token — open it with the link printed at startup",
"文件保存请求格式不正确":"The file save request is malformed",
"文件已在打开后被其他操作修改,请重新载入":"The file changed after it was opened; reload it and try again",
"找不到该文件,它可能已被移动或删除":"The file was not found; it may have been moved or deleted",
"写入目标不在工作区可写范围内。这是文件工具的写入范围,不是操作系统沙箱;要允许写入,请在 设置 → 沙箱 → 额外可写目录 中添加目标文件夹":"The target is outside the workspace write scope. This is the file tools' write scope, not an OS sandbox; to allow the write, add the target folder under Settings → Sandbox → Also writable",
"无法读取工作区文件列表":"The workspace file list could not be read",
"该文件不是可编辑文本或超过大小限制":"The file is not editable text or exceeds the size limit",
"无进展设置未能保存:{detail}":"The progress settings were not saved: {detail}",
"主目录或磁盘根目录不能整体信任,请打开具体的项目文件夹":"A home directory or drive root cannot be trusted as a whole - open a specific project folder",
"未能记下对此文件夹的信任决定:{detail}":"The trust decision for this folder was not saved: {detail}",
"该数值超出允许范围,未做任何修改:{detail}":"That value is out of range; nothing was changed: {detail}",
"查看并管理正在操作这台电脑的设备":"See and manage devices controlling this computer",
"手机扫码即可进入这台电脑的 Studio":"Scan with your phone to open this computer's Studio",
"远程连接已断开":"Remote connection ended",
"这台电脑已停止网页控制。重新连接需要再次验证设备状态。":"This computer has stopped web control. Reconnecting will verify the device again.",
"重新连接":"Reconnect",
"远程 Studio 暂无响应,请检查电脑是否在线后重试。":"Remote Studio did not respond. Check that the computer is online and try again.",
"远程连接已断开,请重新连接。":"The remote connection ended. Reconnect to continue.",
"远程 Studio 拒绝了这次请求。":"Remote Studio refused this request.",
"请先登录 Reasonix,再打开 Web Studio。":"Sign in to Reasonix before opening Web Studio.",
"请先更新这台电脑上的 Studio,再连接。":"Update Studio on this computer before connecting.",
"无法授权 Web Studio,请重新登录后再试。":"Web Studio could not be authorized. Sign in again and retry.",
"连接远程 Studio 超时,请检查电脑是否在线。":"Connecting to Remote Studio timed out. Check that the computer is online.",
"远程中转服务暂时不可用,请稍后重试。":"The remote relay is temporarily unavailable. Try again shortly.",
"远程 Studio 身份校验失败,请停止连接并检查设备。":"Remote Studio identity verification failed. Stop and check the device.",
"需要重新登录":"Sign in again",
"为了安全,远程控制在登录满 24 小时或退出登录后需要重新登录。":"For security, remote control asks you to sign in again 24 hours after signing in, or after you sign out.",
"重新登录":"Sign in",
"远程控制需要重新登录。":"Remote control needs you to sign in again.",
"这台电脑已从账号中移除。":"This computer was removed from your account.",
"连接中断,这次操作可能没有完成,请刷新后确认。":"The connection dropped; this action may not have finished. Refresh to check.",
"互联网远程":"Internet remote access",
"互联网连接二维码":"Internet connection QR code",
"手机扫码 · 不在同一网络也能连接":"Scan with your phone · works away from this network",
"刷新二维码":"Refresh QR code",
"正在生成二维码…":"Creating QR code…",
"显示互联网连接二维码":"Show Internet connection QR code",
"同一网络直连":"Direct connection on the same network",
"速度更快,不经过中转":"Faster, without the relay",
"允许局域网访问":"Allow local network access",
"Web Studio 已连接":"Web Studio connected",
"Web Studio 已断开":"Web Studio disconnected",
"允许手机访问":"Allow phones to connect",
"这台电脑现在没有局域网地址,连上 Wi-Fi 或网线后再试。":"This computer has no private network address right now. Connect to Wi-Fi or Ethernet and try again.",
"手机和这台电脑在同一个网络里,扫码后就能看会话、发消息、批准操作。关闭或退出 Studio 会断开所有手机。":"A phone on the same network as this computer can scan a code to follow sessions, send messages and approve actions. Turning this off or quitting Studio disconnects every phone.",
"二维码只能配对一台手机,{time} 前有效。别把它发到群里或截图外传。":"The code pairs one phone and is valid until {time}. Do not post it to a group chat or share a screenshot of it.",
"显示配对二维码":"Show a pairing code",
"显示局域网二维码":"Show local network QR code",
"互联网连接需要登录同一账号,内容端到端加密;局域网直连只在可信网络中开启。":"Internet access requires the same account and is end-to-end encrypted; only enable direct access on a trusted local network.",
"登录 Reasonix 账号后,可生成在外网也能使用的连接二维码。":"Sign in to Reasonix to create a QR code that also works away from this network.",
"{time} 配对 · 最近 {seen}":"Paired {time} · last seen {seen}",
"断开这台手机?它需要重新扫码才能再连上。":"Disconnect this phone? It will need to scan a new code to connect again.",
"The default model runs this conversation and most tasks, and every other use follows it unless you give that job a model of its own. Switching keeps the conversation and rebuilds the runtime; it cannot be done while a task runs.",
"添加模型服务":"Add model service",
"搜索已添加的服务":"Search added services",
"上移 {name}(Alt+上方向键)":"Move {name} up (Alt+Up Arrow)",
"下移 {name}(Alt+下方向键)":"Move {name} down (Alt+Down Arrow)",
"重命名 {name}(F2)":"Rename {name} (F2)",
"重命名 {name}":"Rename {name}",
"回车保存,Esc 取消;留空恢复为「{name}」":"Enter saves, Esc cancels; leave it empty to go back to “{name}”",
"无法保存服务顺序。":"Could not save the service order.",
"没有匹配的服务。":"No matching service.",
"添加一个模型服务后,在这里查看和修改它。":"Add a model service to see and change it here.",
"The setting above decides whether you are asked; this one decides what is never allowed and what never needs asking. Changes rebuild the runtime and cannot be made while a task is running.",
"无法读取权限配置。":"Cannot read the permission config.",
"当前项目自带权限配置":"This project ships its own permissions",
"当前项目记住的放行规则":"Remembered allow rules for this project",
"已记住的权限规则无法读取":"Could not read remembered permission rules",
"请检查或修复用户目录中的授权记录文件。":"Check or repair the grant record in your user directory.",
"请选择要收回的授权规则":"Select an approval rule to revoke",
"任务正在运行,请先停止再收回已记住的授权":"Stop the running task before revoking a remembered approval",
"当前会话无法重建,授权未更改":"This session cannot rebuild; the approval was not changed",
"无法更新当前项目的授权记录,请检查用户目录中的授权文件":"Could not update this project's approvals; check the grant record in your user directory",
"关闭后安装依赖、拉取仓库等操作都会失败,这正是该选项的用途":"With this off, installing dependencies and cloning repositories will fail — which is what it is for",
"保护被改动的文件":"Protect changed files",
"模型读过或写过的文件若之后被你或其他程序改动,它整文件覆盖时会被拒绝,需要先重新读取":"If a file the model read or wrote is changed afterwards by you or another program, a whole-file overwrite is refused until the model reads it again",
"内置浏览器":"Built-in browser",
"agent 用来打开和操作网页的工具。该开关单独保存,不受命令行版本浏览器设置的影响。修改会重建运行时,任务运行期间无法变更。":"The tools the agent uses to open and operate web pages. This switch is stored on its own and is not affected by the command-line version's browser setting. Changing it rebuilds the runtime and cannot be done while a task is running.",
"无法读取内置浏览器设置。":"Could not read the built-in browser setting.",
"启用内置浏览器":"Enable the built-in browser",
"agent 可以打开网页、读取内容并操作页面;浏览器在第一次使用时才启动。关闭后这些工具不会出现在工具列表中":"The agent can open web pages, read them and operate them; the browser only starts the first time it is used. When off, these tools are not in the tool list",
"当前项目的配置文件开启了它,此工作区仍会提供内置浏览器。":"This project's config file turns it on, so this workspace still has the built-in browser.",
"当前项目的配置文件关闭了它,此工作区不会提供内置浏览器。":"This project's config file turns it off, so this workspace has no built-in browser.",
"How this endpoint expresses thinking depth. It cannot be probed: a relay forwards someone else's model, and only you know what is behind it. Choosing one enables the reasoning level; choosing wrong makes the endpoint reject the request.",
"Which version is installed, whether an update is available, and how to go back if something breaks. A rollback pins the version you chose and is not undone by an automatic update.",
"The installed version, available updates, and how to go back if something breaks. A downloaded update waits for you to choose when to restart; after a rollback the chosen version is pinned andnewer ones stop being offered.",
"What the agent recorded on its own: you never configured it, but it acts on it. Grouped here by when each entry is recalled, with the ones actually used last turn marked.",
"{err} —— 本地功能不受影响,请稍后重试。":"{err} — nothing local is affected; try again later.",
"已固定在 {v},不会自动更新":"Pinned to {v}; updates will not move it",
"固定版本为 {pinned},当前运行的是 {current}":"Pinned to {pinned}; what is running is {current}",
"有新版本 {v}":"{v} is out",
"切换到 {v} 失败":"Switching to {v} failed",
"{err} —— 当前版本未被改动,可以重试。":"{err} — the running version was not touched; you can try again.",
"已固定在 {v},不再提示新版本":"Pinned to {v}; newer versions are not offered",
"回退时会固定在所选版本,免得新版本提示把你带回刚离开的版本。":"A rollback pins the version you chose, so an update prompt does not take you back to the one you left.",
"取消固定":"Unpin",
"这个固定没有作用,下次启动时会自动清除。":"This pin holds nothing and is cleared at the next launch.",
"现在清除":"Clear it now",
"可在下方对应行安装。下载完成后会先问你,再重启。":"Install it from the row below. Once it has downloaded, Studio asks before restarting.",
"有 {n} 项任务正在运行":"{n} tasks are running",
"{v} 已下载并通过签名校验":"{v} is downloaded and its signature checked",
"现在重启会中断它们。可以等任务结束后再重启。":"Restarting now would stop them. You can restart once they finish.",
"重启 Studio 后生效。重启会关闭当前窗口,稍后重启也可以。":"It takes effect when Studio restarts. Restarting closes this window; later works too.",
"较新版本写入的会话在旧版本中暂时无法打开,升级回去后即可恢复。":" A session written by a newer version will not open on an older one until you upgrade again.",
"仍然重启":"Restart anyway",
"立即重启":"Restart now",
"稍后":"Later",
"下载完整安装包":"Download the full installer",
"重启以完成安装":"Restart to finish",
"已下载,待重启":"Downloaded, waiting for a restart",
"正在安装…":"Installing…",
"正在重启到新版本…":"Restarting into the new version…",
"安装失败":"Install failed",
"无法读取 {v} 的发布信息":"Could not read the release details for {v}",
"版本目录暂时连不上,可能是网络或代理问题。当前版本未被改动,稍后重试即可。":"The version index cannot be reached right now, likely a network or proxy problem. The running version was not touched; try again later.",
"{v} 下载中断":"Downloading {v} was interrupted",
"网络中断或超时。当前版本未被改动,可以重试。":"The connection dropped or timed out. The running version was not touched; you can try again.",
"{v} 未通过签名校验":"{v} failed its signature check",
"下载到的文件与发布签名不符,已丢弃,没有安装。多为下载损坏或网络被篡改,请重试;反复出现请反馈。":"The downloaded file does not match the release signature, so it was discarded and nothing was installed. This is usually a corrupted download or a tampered network; try again, and report it if it keeps happening.",
"无法写入更新文件":"The update could not be saved",
"磁盘空间不足,或更新缓存目录不可写。清理空间后重试。":"The disk is full, or the update cache folder cannot be written. Free some space and try again.",
"{v} 没有适用于本机的安装包":"{v} has no package for this machine",
"这个版本没有为本机的系统发布可自动安装的包,请下载安装包手动安装。":"This version publishes no automatically installable package for this system; download the installer and install it by hand.",
"{v} 的安装没能开始":"Installing {v} did not start",
"安装程序被系统或安全软件拦下了。当前版本未被改动,可以重试,或下载完整安装包手动安装。":"The system or security software stopped the installer. The running version was not touched; try again, or install the full package by hand.",
"上次更新到 {v} 没有生效":"The last update to {v} did not take effect",
"重启后运行的仍是 {current}:新版本没能替换已安装的文件,已恢复原版本。常见原因是装在 Program Files 或其他磁盘,或文件被安全软件占用。下载完整安装包安装一次即可,之后的更新会恢复正常。":"After the restart {current} is still running: the new version could not replace the installed files, so the previous one was restored. This usually means Studio is installed under Program Files or on another drive, or security software held a file. Install the full package once and updates work again after that.",
"当前版本未被改动,可以重试。":"The running version was not touched; you can try again.",
"启动时预先连接,工具每轮随请求发送,模型直接调用,省去搜索往返,但占用上下文。":"Connected at startup; the tools go with every request and the model calls them directly: no search round trip, but they take up context.",
"用到时再搜索加载,不占上下文,调用前多一轮往返。":"Searched for and loaded when needed: no context used, one extra round trip before a call.",
// 社区市场
"社区市场":"Community market",
"社区发布、经过审核的技能、插件、MCP 服务与主题。固定了审核内容的版本按审核时的内容安装,未固定的需要你信任发布者;安装前都会列出将写入的全部内容,与粘贴地址安装走同一套确认。":"Skills, plugins, MCP servers and themes published by the community and reviewed. A version pinned to its reviewed content installs exactly that; an unpinned one needs you to trust its publisher. Everything an install would write is listed first - the same confirmation a pasted address goes through.",
"已安装":"Installed",
"发现":"Discover",
"MCP 服务":"MCP servers",
"插件":"Plugins",
"近期热门":"Trending",
"暂无评价":"No votes yet",
"好评 {pct}%({n} 票)":"{pct}% positive ({n} votes)",
"评价":"Vote",
"赞":"Upvote",
"踩":"Downvote",
"撤回赞":"Withdraw upvote",
"撤回踩":"Withdraw downvote",
"登录后评价":"Sign in to vote",
"验证账号邮箱后才能评价":"Verify your account email to vote",
"在这台设备上不能评价":"Voting is not available on this device",
"安装最多":"Most installed",
"最新":"Newest",
"排序":"Sort",
"搜索技能、插件、MCP 服务与主题":"Search skills, plugins, MCP servers and themes",
"搜索社区市场":"Search the community market",
"无法读取社区市场":"Could not read the community market",
"没有找到匹配的包。":"No matching packages.",
"没有找到已固定内容的包。可关闭筛选查看全部包。":"No pinned packages found. Turn off the filter to see all packages.",
"查看全部包":"See all packages",
"只看已固定":"Pinned only",
"未固定":"Not pinned",
"固定状态未知":"Pin status unknown",
"管理员标记可信":"Admin marked trusted",
"可更新":"Update available",
"{n} 次安装":"{n} installs",
"加载更多":"Load more",
"返回列表":"Back to list",
"查看已安装能力":"View installed capabilities",
"{name} {version} 将安装以下内容":"{name} {version} will install the following",
"已按内容摘要核对:与审核时固定的版本一致。":"Checked by content digest: identical to the version pinned at review.",
"这个来源包含 {n} 个技能,会全部安装":"This source holds {n} skills, and all of them will be installed",
"我已看过这 {n} 个技能,全部安装":"I have read these {n} skills - install all of them",
"以下项目需要逐项看过再安装":"Read each of these before installing",
"以下项目会改变可用能力":"These change what is available",
"以下项目仅添加文件":"These only add files",
"无法读取 {name}":"Could not read {name}",
"发布者":"Publisher",
"审核版本":"Reviewed version",
"来源":"Source",
"固定内容":"Pinned content",
"未固定——审核时没有记录内容摘要":"Not pinned - no content digest was recorded at review",
"仓库":"Repository",
"标签":"Tags",
"已安装 {version}":"Installed {version}",
"先列出将安装的全部内容,确认后才会写入":"Everything it would install is listed first; nothing is written until you confirm",
"审核时没有记录内容摘要,需要信任发布者后安装":"No content digest was recorded at review; trust the publisher to install it",
"内容未经审核固定":"Content not pinned by review",
"审核时没有记录内容摘要,无法确认来源现在提供的仍是审核过的内容。只在你信任发布者 @{handle} 时安装。":"No content digest was recorded at review, so there is no way to confirm the source still serves what was reviewed. Install only if you trust the publisher @{handle}.",
"信任并安装":"Trust and install",
"你选择了信任这个发布者":"You chose to trust this publisher",
"以下是来源现在提供的内容,可能与审核时不同。确认后内容若有变化,安装会被拒绝。":"This is what the source serves now, which may differ from what was reviewed. If it changes after you confirm, the install is refused.",
"查看将安装的内容":"See what it installs",
"查看更新内容":"See what the update installs",
"MCP 服务只固定了启动方式与连接地址;它启动后自行下载或远端提供的代码不在固定范围内。":"For an MCP server only how it is started and where it connects are pinned; code it downloads or a remote end serves after that is not",
"技能不会被原地覆盖:先在「已安装」里移除旧版本,再回来安装":"A skill is not overwritten in place: remove the old version under Installed first, then install this one",
// 社区市场:发布
"浏览":"Browse",
"我的发布":"My packages",
"发布":"Publish",
"登录后可以在这里发布技能、插件、MCP 服务和主题,并查看审核进度。":"Sign in to publish skills, plugins, MCP servers and themes here, and to follow their review.",
"审核通过后会在社区市场公开;在「我的发布」里可以看到审核进度。":"It goes public in the community market once approved; My packages shows where review stands.",
"再发布一个":"Publish another",
"查看我的发布":"See my packages",
"以 @{handle} 的名义提交,审核通过后公开。只收来源地址,不上传文件。":"Submitted as @{handle} and made public once approved. Only the source address is sent; no files are uploaded.",
"主题以插件包发布,包里只能有主题;带技能、钩子或 MCP 服务的包请按插件发布。":"A theme is published as a plugin package that holds only themes; a package with skills, hooks or MCP servers is published as a plugin.",
"小写字母、数字、点、下划线、连字符,最多 64 个字符。":"Lowercase letters, digits, dots, underscores and hyphens, up to 64 characters.",
"留空时新包为 0.1.0,更新自动加一个补丁号。":"Left empty, a new package starts at 0.1.0 and an update bumps the patch number.",
"来源地址":"Source address",
"摘要":"Summary",
"描述":"Description",
"用逗号分隔,最多 8 个":"Comma-separated, up to 8",
"没有提交成功":"The submission did not go through",
"提交后进入审核队列;审核员会固定审核时的内容,之后只安装那一份。":"It joins the review queue; the reviewer pins the content they reviewed, and only that is ever installed.",
"提交中…":"Submitting…",
"提交审核":"Submit for review",
"无法读取我的发布":"Could not read your packages",
"还没有发布过。":"Nothing published yet.",
"固定到提交的 GitHub 目录,形如 https://github.com/owner/repo/tree/(40 位提交号)/子目录":"A GitHub directory pinned to a commit, such as https://github.com/owner/repo/tree/(40-character commit)/subdir",
"SKILL.md 的 https 地址,或 GitHub 仓库里某个技能的目录。":"An https address of a SKILL.md, or one skill's directory in a GitHub repository.",
".mcp.json 或仓库的 https 地址,或 npm 包名。":"An https address of a .mcp.json or a repository, or an npm package name.",
"审核中":"In review",
"已公开":"Public",
"未通过":"Rejected",
"已隐藏":"Hidden",
"私有":"Private",
"已保存 {slug} {version},仅自己可见":"Saved {slug} {version}, visible only to you",
"它不会进入审核,也不会在社区市场出现;在「我的发布」里可以安装它,或随时提交审核。":"It skips review and never appears in the community market; install it from My packages, or submit it for review whenever you like.",
"仅自己可见:不提交审核,社区市场里只有你的账号能看到并安装":"Only me: skip review; in the community market only your account can see and install it",
"保存后只有你能看到;要公开时在「我的发布」里提交审核。":"Only you will see it; submit it for review from My packages to make it public.",
"保存为私有":"Save as private",
"仅你可见,未提交审核":"visible only to you, not submitted for review",
"返回我的发布":"Back to my packages",
"无法预览 {name}":"Could not preview {name}",
"正在预览将安装的内容…":"Previewing what would be installed…",
"未审核 · 仅你可见":"Unreviewed · visible only to you",
"这是你自己发布、还没有通过审核的版本。安装会核对与这次预览相同的内容摘要,内容在确认后有变化就拒绝安装。":"This is your own version, not yet approved by review. The install checks it against this preview's content digest and is refused if the content changes after you confirm.",
"内容摘要:{digest}":"Content digest: {digest}",
"长时间无进展":"Long stretches without progress",
"判断只看宿主能观察到的效果,不看命令内容;提示和暂停都不会向模型发送任何内容,也不会拒绝任何调用。":"Judged only from effects the host can observe, never from what a command says. Neither the notice nor the pause sends the model anything or refuses any call.",
"长时间无进展时暂停任务":"Pause a task that stops making progress",
"默认关闭:只在输入框上方提示,由你决定是否停止。开启后达到下面的条件会暂停任务,已完成的工作都会保留,发一条消息即可继续。":"Off by default: a notice appears above the composer and stopping is your call. When on, a task that meets the conditions below is paused; the work so far is kept and one message continues it.",
"连续无进展轮数":"Rounds without progress",
"没有改动文件、没有检查或任务步骤变化、没有读到新内容、也没有子代理返回的工具轮次。默认 {n}。":"Tool rounds in a row that changed no file, moved no check or task step, read nothing new and brought back no sub-agent result. Default {n}.",
"轮":"rounds",
"输入 token 上限":"Input token backstop",
"与模型无关的后备:自上次有进展以来的累计输入达到上下文窗口的这个倍数时同样提示。默认 {n} 倍。":"A model-independent backstop: the same notice once the input spent since the last progress reaches this many context windows. Default {n}×.",
"倍窗口":"× window",
"请输入 1 到 {n} 之间的整数。":"Enter a whole number from 1 to {n}.",
"无法读取无进展设置。":"Could not read the progress settings.",
};
Loaded 100 of 1189 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.