TestApplyLocalSkillLinkMode asked runtime.GOOS, and asked it after asserting
resp.OK — so a machine that cannot create a symlink failed on the refusal the
tool had correctly reported, rather than skipping. Windows allows symlinks under
Developer Mode or elevation, which the platform name cannot tell apart from a
plain account, so the check is now the read three cases in this file already
make: try it, and skip on the error the filesystem gives back. A Windows host
that can make one now runs the case instead of skipping it.
Also ignore *.exe~ — what the linker leaves when it cannot replace a binary that
is still running, which on Windows is every rebuild while Studio is open.
The shelf's pills were ordinary flex children, so a row one pixel short of
fitting squeezed them instead of wrapping — and Chinese breaks between any two
characters, so 计划 came apart into two lines and took the row's baseline with
it. A pill is now its label's width or nothing: the shelf wraps, which .row
already allowed, and the send button's auto margin keeps it at the right end of
whichever line it lands on. Verified from 980px down to 320px: one line, two,
then three, with nothing broken at any of them.
The model pill is the one control with no width of its own — a gateway can
publish an id longer than the shelf — so it gives up characters before the
shelf gives up a line, and the ref it was shortened from stays in the tooltip.
Closes#9455
The attach pool has always keyed spaces by workspace and named a forward per
one; the far kernel remembers every folder a pane opens and answers /tree with
all of them. Only the book could name a single directory, so nothing could say
the second one — and cold, before a pane exists to ask through, the sidebar had
just that one row.
RemoteHostEntry gains workspaces beside workspace, folded by WorkspaceList into
one default-first list so a row that only ever set the old field is a list of
one rather than a case every reader repeats, and an upsert makes the head of
the list the default the CLI still connects to. The sidebar shows the book and
the far tree together: a folder added here and never opened over there stays,
and one opened over there appears without being written down here.
The [remote] renderer moves to render_remote.go — the section is one subject,
and the function it came from is already the longest in the package.
Closes#9459
The kernel replays a pending ask/approval to every client that attaches, and
the window then overwrote its items with what /history rebuilds — where the ask
tool call is deliberately drawn as nothing, because ask_request carries the id.
The pane sat at 等你决定 with nothing on screen to decide and the queued turn
behind it never dispatched. A prompt is not part of the record; it is the run
stopped waiting on an answer, so it rides across the rebuild. prompts.ts also
gives one its kernel id, so a replayed frame is the same card rather than a
second answerable copy, and an answered one is not reopened by the frame log.
SandboxSettings carries effectiveBash beside effectiveWriteRoots, so the pane
draws what the confiner will do rather than what was configured: an unset mode
enforces everywhere but Windows, which runs unconfined however the file reads.
The segment for a mode this host cannot run is visibly dead and carries its own
reason — `disabled` alone kept the hover of a live control and then swallowed
the click, which is what was reported as broken. Enforce-without-a-backend
refuses by code rather than by a hand-written sentence, and the SandboxSettings
mirror is declared to wire-parity so the next field cannot drift.
Closes#9454
errmsg and the refusal-hint test, instruction's resolver, and the i18n message
tables the two of them read from.
Accumulated working-tree work, committed as found; the tree builds and passes at
the tip.
install, launch, platform and the Windows shell case behind one bootstrap, with
the hardening and pure-function tests that let it be exercised without a machine
on the other end, and the hub and CLI entry points that reach it.
Accumulated working-tree work, committed as found; the tree builds and passes at
the tip.
reasoning_replay for OpenAI and Anthropic, the h2 reset case, stream_failure and
the retry path around them, with doctor reporting what the provider actually did
so a broken stream is diagnosable instead of just slow.
Accumulated working-tree work, committed as found; the tree builds and passes at
the tip.
The palette, the picture-less sky and the ray/cloud tints the Studio design was
drawn against, as a pack the kernel serves rather than something a user has to
install to see the app as designed.
Accumulated working-tree work, committed as found; the tree builds and passes at
the tip.
RunStrip was one strip carrying every reading. It splits into panels — Overview,
Runtime, Cache, Cost, Context, Agents, Files, Jobs — with derive.ts doing the
walk once in the pane and kit.tsx holding the shapes they share, so a reading
that grows does not widen a row every other reading has to live in.
Alongside: Sky draws the backdrop a theme pack asks for rather than placing a
picture, Nav takes the icon rail, Timeline and Task give the trajectory and a
subagent their own views, AccountRow the account, and prefs/keys/trailing carry
the state, the bindings and the tail the panes were each keeping privately.
perf grows three scripts: selfcheck, which pushes a false assertion into every
guard to prove the guard can still go red; sourcemap, which turns a minified
frame back into a source position; and switchprof, which samples the cost of
opening a long session. look and verify grow the assertions they were missing.
This is accumulated working-tree work, not written in the session that committed
it; the message describes what is here rather than the reasoning behind each
piece. The whole tree builds, vets, typechecks, and passes its tests and
repolint at the tip.
Both stand for a failure that stayed silent while every other gate was green.
tokens.mjs: a var(--x) with no fallback has to name something someone assigns.
CSS drops the whole declaration when it does not, and the property falls back to
whatever was inherited, which reads as the design. Two of those were live in the
tree — the connection prompt's scrim named a --bg that has never existed, so it
had blur and no dimming at all, and the usage panel's --fg meant its text colour
never landed. Both lists are read from source, the stylesheets' own `--x:` and
the per-element ones JSX writes into style, because a hand-copied list rots.
shipped.mjs: dist must be newer than everything under src. A build can succeed,
print its tick, and compile something else — this tree has more than one process
on it, and a stash that put it back to HEAD for forty-eight seconds was enough
for one build to fall inside the window and produce the previous release. The
window then loaded it and the whole interface reverted, while the tests stayed
green: they read the source. Timestamps rather than marker strings, because
"the artifact is older than the source" is one sentence that cannot rot.
Both were mutation-tested the way selfcheck.mjs asks: a var nothing defines
turns tokens.mjs red, touching a source file turns shipped.mjs red, and a
PERF_SRC that points nowhere fails loudly instead of passing quietly.
Six things, and four of them were defects rather than taste.
The window opened on a blank page. index.html now paints a boot screen before
the bundle it would otherwise wait for exists: the wordmark written on letter by
letter in gilt and settling to ink, the sky the theme packs draw, and one line
saying the kernel is being reached. main.tsx hands the window over when the
intro has finished rather than after a counted delay, so a fast kernel does not
flash it and a slow one is what it is for. It carries its own palette because
tokens.css is a bundled import that does not exist yet at that point.
Settings becomes a sheet over the workspace instead of a second full-screen
window. That removes the duplicate: .prefs-hd used to be a title bar too, with
its own drag region, its own traffic-light inset and its own window controls.
The window frame is .chrome and only .chrome now, and the veil starts below it,
so the window can still be dragged, minimised and closed while settings is
open — a modal that traps the window buttons is not a modal. Its contents list
grows the design's marks and groups, and reports what each section is worth:
storage in bytes, usage in tokens over the window Usage itself opens on, the
build running.
The segmented control was a full-width strip of underlined tabs at all sixteen
call sites. It is an inset track with the selected cell lifted out of it, which
is a control's size rather than a navigation bar's, and section headings shrink
to overlines so the rows under them carry the weight.
Then the defects:
- A theme's picture is body::before at z-index -2, and it is visible only
because an uncoloured html hands the canvas to body's background. The boot
screen's stylesheet had coloured html, which takes the canvas over and
demotes body's to an ordinary element paint — landing on top of the picture.
A wallpaper simply stopped appearing. boot.test.ts fails any rule that puts a
background on html or :root, because nothing else was watching that file.
- The cost panel's sparkline was a black rectangle. Spark's fill and stroke
rules were written under .rate, the throughput row's scope, so the cost
panel's copy matched none of them and an SVG with no fill declared is filled
black. The colours belong to the mark; only the throughput row's geometry
stays scoped. The panel's wrapper stops sharing the component's own class,
which is what let a wrapper's layout land on the drawing inside it.
- A picker's menu jumped to the bottom of the window before fading out. Its
anchor name hangs on [aria-expanded="true"], which flips the instant the menu
is asked to close, while the menu is still on screen; with no anchor left to
resolve, an anchor() without a fallback invalidates the whole declaration.
The fallback is the same 100% the non-anchored rules have always used.
- Every view swap cross-faded a snapshot of the entire document. Only the
swapped element carries a view-transition-name, which swap.ts says in a
comment, but the root kept its own default name — so closing settings dipped
the whole window and brought it back. :root opts out.
The preview itself is a sheet in the same family, portalled to the body because
under a wallpaper the side column carries a backdrop-filter and that makes it
the containing block for anything fixed inside it. Rows in the changes list
become buttons carrying git's own letter, coloured by what it means.
--fg was read six times in the usage panel and has never been defined, so those
declarations were dropped and the colour fell back to whatever was inherited.
It is --text. bytes() ran to MB and stopped, which is how a 9 GB total read as
"9318.4 MB"; it runs to TB, and Storage stops carrying a second copy of the
rule that i18n/format exists to hold.
The rail lists what the tree differs by, and until now that was all it could
say: a path and a porcelain letter. What the file actually differs by lived
only in the tool events of the turn that wrote it, so a change made before this
session — or by a shell command no event describes — could be named and not
read.
gitstatus.Diff answers for one path. It measures against HEAD so a staged edit
and an unstaged one read as the single change they are to a person. An
untracked file has nothing in HEAD to compare with and is diffed against the
null device instead: that is the only way git prints it without first writing
to the index, which a read must not do.
The path arrives from a client, so safeRel keeps three shapes away from git —
outside the tree, absolute, and readable as an option — and every invocation
puts it after "--" as well. ErrPathOutsideTree is a sentinel because the caller
has to tell a bad request from a broken repository; over HTTP they carry
changes.path_outside_tree and changes.diff_failed, and the frontend has words
for both.
root goes through gitcmd's dir parameter rather than an "-C" argument. gitcmd
hardens a subcommand it can see at args[0], and --no-ext-diff/--no-textconv are
exactly the flags a diff against someone else's repository needs: a repository
config can name a program git then runs during an ordinary read.
The desktop shell needed telling too. Its route table lists families rather
than leaves, so /changes/ joins the prefixes — without it the shell answered
/changes/diff with index.html and the preview would have failed on a body that
parses as HTML. TestEveryRouteTheKernelRegistersIsRouted caught that before it
shipped, which is what it is for.
routes() moves to routes.go. serve.go sat exactly on its file-size budget, so
one more registration line broke it, and the budget is not there to be widened:
the route table is every path this server answers, which is a subject of its
own rather than a section of the file that assembles the server. serve.go drops
from 1459 lines to 1365.
packagesFor took the directory of a declared file with filepath.Dir, which
on Windows answers `internal\control`. That is not a pattern go test can
match, and it sorted the list wrong on the way out, because a backslash
outranks a slash — one root cause, two symptoms in the same assertion.
The gate itself runs on ubuntu, so nothing it gates was affected; its test
runs on the whole matrix, and has failed windows-latest since the gate
landed. A declared glob and a go package pattern are both slash-separated
whatever the host is, so both reads move to path.
Windows groups a taskbar button and routes a toast by the process's
AppUserModelID, and the Start Menu shortcut has to carry the same one or
the notification is dropped. The two calls that set both lived in the
retired shell. When it went they lost their caller, and the sweep that
deletes unreferenced exports took them — leaving internal/notify sending
toasts under an identity nothing claims, and six unused findings on the
GOOS=windows lint leg, which the sweep's own host-GOOS run never saw.
Restored and called from the window's startup, before the first window
exists, which is when Windows reads it. The call site carries no build
tag: identity_other.go already answers for every other platform.
A host decorates its pane's sink once — the status icon's fold, desktop
notifications, usage counters — and builds the controller on it. That
decoration lived only at the build call site, so every path that rebuilds
the controller wired the replacement to the bare broadcaster and dropped
it: a model or effort switch, an extension reload, a workspace change,
and saving a provider credential, which is the first thing a new install
does.
Nothing corrected it afterwards. A switch is refused while a turn runs,
so the last event those observers saw was the previous TurnDone, and they
stayed on it for the pane's whole life. The tray reported "nothing
running" through a fourteen-minute turn.
The Server now carries the sink its controller emits into, so the thing
that rebuilds owns it rather than the call site that happened to build
first. build and rebuildWith move beside the options they read; the file
they left is over its ceiling and had no room for the addition.
The test fails on the old code for both rebuild paths with the reported
symptom: the pane's decoration sees none of the replacement's events.
Reported in #9448. The paint ordering fixed in db030ad0 is a real defect
but needs two panes changing at once, and is not what that report shows.
Both have failed every run of CI's windows-latest job since the corpus
landed, and neither is a flake.
gen-topo-cycle timed each call with signal.alarm. Windows Python has no
SIGALRM, so the grader raised while installing the handler, before it
graded anything. A daemon worker thread keeps the semantics exactly —
_Timeout on a hang, the original exception re-raised, so the cyclic case
still reads ValueError — and cannot hold the interpreter open at exit.
gen-csv-quotes compared cli.py's stdout against an expected string.
Windows Python writes CRLF in text mode, so that comparison could never
hold; the carriage returns come off first, because the grader checks the
fields and not the host's line endings. Its mktemp template also read
TMPDIR, which under Git Bash is a Windows path — the rest of the corpus
asks for a bare mktemp -d, and now so does this one.
The modernize pass reports two findings on this file, and they fail both
`make lint` and CI's lint job. A strings.Index used to slice a prefix off
is strings.Cut, and ranging over a Split is SplitSeq.
Two panes changing at once take their folds under the lock and report outside
it, so the two reports could reach the icon in the opposite order. The one that
lost left the icon describing a state the tracker had already moved past, and
nothing corrected it: the fold reports only what changed, so a wrong paint
latched until the next real change. A pane running a fifteen-minute turn is
fifteen minutes of a tray that says nothing is running.
Painting is now serialised and describes the fold as it stands when it paints,
so a report that lost the race repaints the truth instead of overwriting it.
The test fails on the old code with the reported symptom exactly: the icon
shows Working:0 while the tracker holds Working:1.
Reported in #9448.
The review gate read one index for two questions — what the change set is,
and how fresh a review of it must be — so a later harmless write moved the
window past everything before it. A sensitive edit followed by a README write
scored Low and dropped the whole structured-review demand.
UnreviewedMutationBaseline now answers the first question and the latest
mutation still answers the second. It advances per review kind, so an accepted
review does not discharge security_review's obligation with it.
Alongside, the parts of that gate that promised more than they did:
- A blocking verdict is honored at every role setting, and review subagents
submit the typed report everywhere. A verdict a caller must act on carries
an identity, not a sentence. The nudge runs everywhere too; the hard failure
stays in Delivery, since outside it no report is no block either way.
- Medium no longer demands a structured review "or" host-proven
self-inspection. That was no alternative — the reader there is the author —
so the cheap side was always taken and the demand never bound. The real
requirement inside it moves to the self-inspection gate and grows teeth:
inspect every changed path, fresh per path, not one of them.
- warn findings were collected into a slice nothing read. They now reach the
user, and they are collected from any review the turn ran rather than only
from one the gate asked for.
- verificationGapResult.advisory carried no text and reached nobody while two
comments called it reported. The waiver it stood for is right and stays; the
flag wearing a message's type is gone.
The review and security-review bodies stop hardcoding git: fileutil owns one
declared store table, environment reads it, and a skill subagent's prompt now
carries the workspace fact its parent always had. They also drop the ~12-call
self-cap, which was well under the host's real budget, and split the burden of
proof — adversarial about what must be read, conservative about what may be
reported. A pass is a negative claim and owes what any other does.
complete_step: manual evidence no longer stands alone in a turn that already
has receipts. The evidence ablation arm reaches the tool schema, so the two
arms stop paying the same prompt and the A/B can answer for the contract
rather than only for the gate.
agentgraph's own account of why it exists is that every consumer folds the same
deltas, so the graph a headless run reports is the graph the desktop draws. The
desktop's fold is a second, hand-written copy — the type and the merge both —
and nothing checked the two carried the same fields. Adding one field to the
kernel this week meant remembering to add it in two more places by hand. The
next person will not remember, and neither toolchain will say so: they are
compiled separately and meet only at runtime, over JSON.
wire-parity reads the Go struct's json tags against the TS interface's
declarations and fails either direction — a field one side sends and the other
cannot read, and one it reads that nothing sends. A field the encoder skips is
not on the wire and is not demanded, so an internal field costs nothing.
The pairs are declared rather than inferred, for the reason the sensitive paths
are: no spelling tells a mirrored contract from a struct that merely carries
json tags. Only the graph is declared today, because it is the one that claims
both sides fold identically.
agentgraph's own account of why it exists is that every consumer folds deltas
with the same Apply, so the graph a headless run reports is the graph the desktop
draws. The desktop's fold is a second, hand-written copy of it, and nothing
checks the two carry the same fields — a wait cause published by the kernel and
unknown here is a node the picture cannot say anything about.
The type is what holds it: dropping it fails the typecheck through this fold and
its test. The merge line is consistency with every other field rather than a
defence — an absent key already survives the spread.
Rendering is left alone. Every site that would show it, and the strings it would
need, are mid-change in the working tree, and this is one row away whenever that
lands.
Queued was one word for three constraints with two remedies. Two are session
settings — the total sub-agent ceiling and the concurrent-writer ceiling, which
binds a writer while total capacity is still free — and the third is a write
path someone else already holds, which no setting releases: only disjoint paths,
or an edge that orders the pair. The scheduler was the only thing in a position
to tell them apart, and it answered in a sentence built for one caller's error
message, so nothing downstream could.
WaitCause joins the graph's vocabulary beside NodeState and Grant, canStartLocked
returns it instead of prose, and an acquire that has to wait reports it once —
after the unlock, because the callback reaches a sink the scheduler must not be
holding its mutex for. The cause survives the run starting, the way the stamps
do: the state says running now, this says what it waited on.
The fan-out price splits with it. One figure summed every wait between ready and
running while its name said the concurrency ceiling caused it, so a member held
behind a claim was quoted back as a reason to raise a ceiling that would not have
moved it. Two figures now, because there are two answers.
Both fan-outs take the same pair of hooks from one place: the two moments only
the scheduler can report are published by one thing rather than copied into each
producer.
parallel_tasks takes a model and an effort per task, so two members of one group
are not necessarily the same worker. The graph drew them as if they were: a
fleet node carried the profile, model, effort and grant its item ran under, and
these carried a label and nothing else. A reader asking why one arm of a fan-out
cost more than another had nothing on the picture to answer with — the tool
offers the distinction and the graph erased it.
The identity is resolved once, in the loop that was already building the labels,
rather than again inside each item's goroutine.
A run can be told to finish its list by two host prompts: the readiness
continuation, and the Goal FSM's "signaled complete but issues remain". Only the
first was ever taught which actions the completion guard lets through. The Goal
path still ended with "use todo_write/complete_step to mark done" — the one
action the guard refuses — so a run reading it beside the requirement it embeds
was handed the open door and the shut one in a single message, and heard about
neither turn exit.
Both now append one string. Owning it once is the point: the failure here was
two copies drifting apart, and a third copy is what let them.
That third copy was the requirement's own. Every path that shows a requirement
to a model goes through one of these two prompts, so its ways-out sentences were
read twice, in two wordings, inside one message. Its other readers — the ACP
turn outcome, the recovery card — are host- and user-facing, and were being
handed instructions written for a model.
The argument contract read which fields a call supplied and never what it put in
them. A field with the right name and the wrong kind was therefore neither
unknown nor missing: it passed every check the host makes, and the answer came
from Go's unmarshaler — "cannot unmarshal string into Go struct field .evidence
of type []agent.blockedEvidence". That names the host's types rather than the
caller's mistake, and it costs the round trip that discovers it. A live run
spent one that way on conclude_blocked before it even reached the two it spent
on the field names and the evidence itself.
The schema already declares each property's kind and the arguments already carry
one, so comparing them is the same structural read the contract was doing on
names. A mismatch now refuses the call before permission is consulted, for the
reason the missing-field case already refuses: execution was never possible.
A null is not a mismatch, and a property whose schema declares no type leaves
the question unanswered rather than broken — the contract may only report what
the schema actually said. The hint drops "it accepts ..." for a pure mistype,
because the field is accepted and listing the accepted names answers a question
nobody asked.
The contract is read after proxy resolution, so a tool reached through
use_capability is compared against its own schema and refused the same way; a
test pins that, because reading the call order is how you get it backwards. An
on-demand MCP tool declares no properties and the contract stays silent on it. A
connected one is compared against its server's schema — the same trust the host
already places there by refusing a call that omits a field it marks required.
addTurnExitTools registers the two ways a turn ends on the model's terms rather
than the host's: ask for a decision only the user can make, conclude_blocked for
a task that cannot be done as specified. The host's own account of the ways out
named only the second. So a turn that ended by putting a question to the user
was continued straight past it, and the run — told in the host's voice that
there were exactly three doors and that none of them was "wait" — answered the
question itself and said so in the transcript.
Naming ask closes it, in the requirement and in the continuation alike, with the
rationalisation it has to beat spelled out: not having heard back is not an
answer to assume one from.
The escape that used to exist was an accident. The stall counter compared the
gap against the best round so far, and a run waiting on an answer could not
shrink it, so two rounds handed control back. Scoring the continuation against
the previous round's signature instead removed that side effect without
replacing it with a deliberate one.
Nothing here reads the wording of what the model said. The signal is the
declared turn-exit set, which the host was already free to name and did not.
The fleet plan proved a dependency DAG and published it, and the scheduler that
decides which item actually runs never saw it: an AcquireRequest carried a write
claim and a label and no position in the graph at all. So a slot went to whoever
queued first, which is how the head of the longest chain ends up behind leaves
that unblock nothing — and that wait lands on the run's makespan one for one.
It is the loss fanoutMetricsOf already prices as wall above the critical path:
measured every run, acted on by nothing.
The plan now measures each item's rank, the longest chain of dependents below
it, and carries it to the acquire, where the pump drains waiters heaviest first
and keeps arrival order within equal weight. That covers every grant after the
session's slots are full, which on a fan-out wider than the ceiling is nearly
all of them.
It does not cover the first fill, and sorting the launch loop does not either.
Which of a burst of goroutines reaches the scheduler first is the Go runtime's
decision: launching heaviest-first captured 41.8% of the ideal first-fill weight
against declaration order's 51.9% at a ceiling of two, and tied at one and three
— never better, sometimes worse, because the runtime favours the goroutine
created last. The loop is left alone and says why.
A run also queues before it builds rather than after. It used to construct the
whole child — registry, runtime, and the transcript that holds a store lease —
and only then join the line, so a fan-out wider than the session ceiling held
one open per item it had not started, and every millisecond of that setup was
billed to a concurrency ceiling that had not caused it. Foreground runs hold the
slot from before the transcript exists; background ones still return a job id
first and queue inside the job. Deriving the request moves to slot.go, the file
that owns the acquire.
Delegation makes three claims and upstream-edge can only settle one: both of
its tasks are strict chains, so their speed-up is 1.00x by construction and no
scheduler decision is observable in them. fanout-width fixes a shape whose
three research items share nothing, which leaves the session concurrency
ceiling as the only thing deciding whether they overlap — so the serialised arm
needs no ablation flag, only max_subagent_concurrency = 1.
Each subsystem retires nine of ten modules and names the live one in entry.py,
so a grep over the markers returns ten candidates and answers nothing.
inboxEventSink hand-forwarded seven optional sink capabilities and missed
three, so every child run's receipt died one wrapper below the sink that
accounts for it. A benchmark then reported four fleet members as a task one
agent had solved by itself, and the whole delegation section rendered empty
while its owning tests stayed green — which is the failure AuditForwarder's own
doc comment already described. Embedding it is that comment's remedy: a channel
added there now reaches this wrapper without editing it.
The graph already carried every stamp a benchmark needs and nothing folded
them, so delegation was reported as a count of children and never as what the
shape bought. Work against wall is the speed-up; the critical path is the floor
the declared dependencies impose, and wall above it is scheduling loss; slot
wait is what the concurrency ceiling cost members with nothing left to wait for
— the one wait no edge on the graph can draw.
The effect test now pins the stamps themselves. Without them every figure here
folds to zero, which reads as a fan-out that took no time rather than as a
measurement that never happened.
The pane already replayed the trajectory into the graph reducer, but the wire
log dropped every graph_delta on the way to disk, so a reopened window drew a
run that had waited on nothing. A dependency and an adopted answer exist
nowhere else in the stream: unlike a tool mark, they cannot be re-derived.
Studio never called crashreport at all — only the CLI did — so not even a Go
panic was recorded. That is the smaller half. Every Studio crash reported so
far has been a fatal signal inside GTK or WebKit, which no recover can reach:
the runtime writes that trace straight to the descriptor, so a tee on
os.Stderr would keep every log line and lose the only one worth keeping.
So stderr is redirected, not wrapped. The window opens a log under
<home>/logs and dups it onto the descriptor, keeping a copy of the terminal
first when there was one, so running Studio from a shell still prints. One
log per launch, newest few kept: a crash is reported after a relaunch, never
during the run that produced it.
Linux uses Dup3 rather than Dup2 — linux/arm64 has no dup2 syscall at all.
Windows says it cannot instead of pretending: it resolves the handle its
crash writer uses once, at start, so moving os.Stderr afterwards would move
the window's own logs and not the trace. There the log still gets those.
crashreport can now name the host it came from. sanitizeReport overwrote
Source with "cli.go" unconditionally, so a Studio report would have arrived
claiming the CLI and sent whoever read it to the wrong code. The set is
closed rather than free-form, and the CLI's own reports are unchanged.
Motivated by #9408, where the crash is inside cgo and the Go traceback stops
at gtk_main(). The only trace anyone has is there because the reporter
happened to launch from a terminal.
A census of the 78 fields, by where each is written rather than by reading
the names: 37 are bound once in New and never reassigned, and the rest change
while the session runs. Both kinds sat in one list, so c.policy and c.turn
read alike at every call site and only one of them can change under you —
which is the question anyone reaching for c.mu has to answer first.
The 37 move into an embedded controllerDeps. Embedded because field promotion
keeps all ~720 c.field reads resolving unchanged: the point is to make the
lifetime legible at the declaration, not to rename the package. Controller's
own field list drops to 42.
Two fields that qualified by the write test stayed out on purpose. commands
is an atomic.Pointer and externalFolderToolRefs sits with the mutex-guarded
map it belongs to; filing either under "never rebinds" would read as "not
concurrent", which is the opposite of true.
repolint reclaims struct-state 5, file-size 123, function-size 5 and essay 6
— the scalar debt recorded against controller.go was exactly this mixing.
Tests, vet, and -race on the package all pass unchanged.
The memory panel has always told the reader that saving "records a new
revision; the old one stays". Nothing in the UI could show that history or
step back into it: GET /memory/revisions and POST /memory/restore were both
served, both had Controller port methods behind them, and the frontend never
named either — the promise was there and unverifiable.
An entry past its first revision gets a history control; opening it lists the
revisions behind the current one with their dates and bodies, each restorable.
Restoring appends a revision rather than rewinding to one, which the panel
says, because a restore that dropped later revisions would destroy the very
version a reader opened the history to compare against.
The fixture kept only the latest revision, so a panel driven by it would have
shown an empty history and proved nothing; it now keeps what a save replaced,
the way the store does.
Verified in the running Studio through CDP: editing an entry produces
revision 2, the history lists revision 1 with its original body, and
restoring it leaves the entry on revision 3 carrying revision 1's text with
both older revisions still listed.
REASONIX.md calls these paths the ones where a wrong change is "a hole in a
boundary". Measured, they were the least-tested part of the kernel rather
than the best: shellsafe 59.3%, shellparse 63.6%, sandbox 69.4%, netclient
47.2%, against a tree median of 76.5%. The code deciding what the agent may
execute and reach had the weakest evidence that it works.
The gate reads the same `sensitive:` bullets the runtime reads for its
review requirement, so a path is declared once and gets both effects — there
is no second list to fall out of sync. Floors are per declared path and
measured from the coverage profile per file, not per package, because
`internal/control/approval.go` is sensitive while the package around it is
not; measured on its own it is 93.9%, which no package number would show.
Floors ratchet in the opposite direction to repolint's budgets and for the
same reason: raising one is free, lowering one needs -allow-drop and a
reason in the PR. They record what is there today rather than an aspiration,
so the gate is green on arrival and only fails on a regression.
Verified by removing one shellsafe test file: the gate reports 47.3% against
its 59.3% floor. Adding an unrelated `sensitive:` line puts that path under
the gate with no other change.
Two fixes and one sweep, all from the same audit.
The process history catalog was never closed. boot's cleanup releases LSP,
plugin hosts, the runtime scope and session temp; the catalog's own
Flush/Close had lived in the retired shell and were left to two test files.
It is process-wide, shared by every pane and workspace, so it closes at the
process boundary next to the usage catalogs, not with any one controller.
Queued index writes are drained first — the reopen after an unclean exit
re-scans every root instead.
The sweep removes 57 exported functions with no reference anywhere in the
tree, tests included, plus the private helpers that fell dead behind them.
Iterated to a fixpoint: deleting a function exposes the next one, and both
the orphan rule and golangci's unused ran again after each round until
neither reported anything.
The rule learned two things from being wrong here. Methods are now out of
scope: chatTUI.Init is bubbletea's tea.Model, no file in the tree names it,
and proving that needs type information an AST pass does not have — the
first attempt at this sweep deleted it. And an orphan tests still reference
is reported differently from one nothing references, because they need
opposite fixes: repair.PrepareFileUpdate is a cross-package fixture eight
test files build state with, and deleting it was the second thing that
attempt got wrong. Only the unambiguous half is deleted here; the 124
remaining stay recorded.
The last-known-good snapshot died because deleting one frontend left its
callee behind with the implementation, the tests, and the doc comments all
intact. Nothing in CI reads "has a caller", so the tree stayed green for
eight days while a recovery path read a file no longer written.
The orphan rule counts callers across the whole tree, which is the part a
single-module analysis cannot do: desktop/ is its own module and a real
consumer of internal/, so a scan that stopped at the root module would call
its APIs dead. Two things deliberately do not count as callers — tests,
which are what keep an orphan warm and green, and doc comments, which are
what make one look wired. Both are checked by tests here, because both were
true of RecordHealthyConfig.
Test-support packages are exempt: their exports cannot move into _test.go,
so having no production caller is their normal state.
328 existing orphans are recorded as the ratchet baseline, not fixed here.
Verified against the original accident: reverting the fix from the previous
commit trips the gate on RecordHealthyConfig by name.
Recovery reads a snapshot nothing had written since the Wails shell was
deleted. That shell held the only two RecordHealthyConfig calls, so from
908bc3ec0 on the read paths in config/load.go and config/unparsed.go kept
restoring whatever config was current that day. On this machine the file
was 16 days and 161 diff lines stale, and `doctor repair --apply` would
have written it back over the live config without saying so.
The shell proved health with "the window became visible", a signal only
Wails had. A successful assembly proves the same thing for every frontend,
so the record rides BuildRuntime, which reload goes through too, and CLI /
serve / Studio inherit it instead of each re-wiring it. Options.Version is
the host declaring a real installation; embedded and test assemblies leave
it empty and write nothing. The writer is already idempotent — unchanged
config hashes to the existing snapshot — so every start can call it.
Both recovery paths now date what they are about to trust: the load warning
carries the snapshot's age, and doctor repair prints it before --apply
rather than after.
Options moves to its own file, which is what pays for the new field: boot.go
drops 128 lines and the tree reclaims 176 file-size, 21 function-size, 15
essay and 3 complexity of budget.
Verified against a real binary: assembly writes the snapshot and stamps the
ldflags version, a second run adds none, and a broken config reports
"--apply would restore a snapshot 16 days old".
govulncheck reported seven reachable call paths into the 1.5.0 advisories —
the feishu adapter's socket, and two readers that reach a compressed frame
through io.ReadAll. It now finds no vulnerability this code calls.
collectSink appended to an unsynchronised slice and handed the slice itself
back. That held while a turn was the only thing emitting. It stopped holding
when the inbox gained a change listener: the store notifies from its own
goroutine, so six tests began failing under -race — both stacks inside the
helper, one appending while the other read.
It appends under a lock and returns a copy taken under the same one. The race
was never in the controller; the harness had no way to say so.
One cost line mixed the rounds that carry the session with the single-purpose
calls beside them — titles, digests, classification. They scale differently, so
a run that got dearer said nothing about which half moved.
The split is by usage source and named the other way round: the conversation
sources are listed, so a source added later counts as auxiliary instead of
falling out of the split with nobody noticing.
Failing the pristine seed is only half of an authored task. A grader that can
never pass is indistinguishable from one with no solution: every attempt scores
zero, and a corpus of those teaches that nothing is ever accepted. Tasks that
ship a reference solution are now checked against it, and a generated task that
cannot satisfy its own grader is deleted rather than patched.
The generated tasks live apart from benchmarks/e2e on purpose. That suite is
the instrument every cross-version comparison is read against; mixing new
tasks into it would change what those numbers mean.
Recorded runs are only training data once someone decides which ones were any
good, and the run report already holds that verdict. sft mode reads the two
together: the report decides what is exported, the trajectory's run header
supplies the prefix each sample was produced under.
A run without a header is skipped rather than exported against a guess at what
its prompt and tools were.
A trajectory held the event stream and nothing from the request side, so a
reader could see what the model said and never what it was asked — the system
prompt and the tool schemas the rounds were sampled against were simply gone
by the time anyone read the file.
The header carries both, plus the hashes usage events already report, so a
reader can prove the pair belongs to the rounds that follow instead of
assuming it from the file it was found in. It is written by the frontend that
owns the recorder rather than forwarded through the sink chain, which keeps it
out of the optional-capability contract every wrapper would otherwise carry.
NormalizedToolSchemas exports the exact bytes ToolsHash covers, so the hash is
recomputed from what was stored rather than from a second serialization.
bindRunSession collects what a headless run does to attach to its session,
which had grown into four steps inline in runAgent.
Opt-in metrics carried the anonymous install id so the dashboard could show a
deduplicated daily user per signal. Keeping that answer meant keeping a row per
install per signal per day — ~28M of them — and the 30-day COUNT(DISTINCT
install_id) over that table was the database's entire read bill: the primary
key leads with `date`, so each query could prune only the window and scanned
the whole table, eight signals an hour, ~5.4B rows a day for one module.
The client stops sending the id, the worker stops writing and creating the
tables, the hourly rollup that existed only to make that query survivable is
gone, and the dashboard's users/opens toggle goes with it. The aggregate
tables keep the same signals as event counts in ~180k rows.
migrate-drop-metric-users.sql drops the four retired tables. Deploy the worker
first — it stops creating them at runtime, and a DROP before that is rebuilt on
the next request.
Dropping a folder from outside the workspace mints a token and registers it
with the read tools' resolver. That is where the mapping to a real directory
lives, and the grant is read-only — handing bash the real path instead would be
a writable way into it. So the token is not a path, and bash's answer for one is
the shell's own: "No such file or directory".
A model that was handed the token in the same turn believes that. This session
did: it ran `ls` on the token, then `find` over the whole home directory, and
concluded the reference was virtual and the file did not exist — while the file
sat on the Desktop the entire time, reachable by read_file all along.
Bash now recognises a token before running and says what it is and which tools
resolve it. Recognition, not a guess about what a word might mean: the resolver
is asked, and its keys were minted by the host.
The four preflight refusals repeated the same four assignments before returning,
so they are one call now — adding a fifth check left the function shorter.
The posture a pane opened in was read off whichever runtime happened to be
first in the hub's order. Closing the conversation the person had set to auto
or yolo therefore reset the next one to ask — an approval prompt on a tool
they had already waved through, with nothing on screen saying why — and with
every pane closed there was nobody left to read it from at all. The same line
dereferenced Server on a remote pane, which does not have one.
The hub holds the stance instead: seeded from the runtime the host adopts, so
it is the launch setting until someone changes it, and written by the composer
switch, which already persisted the same choice to config for the next launch.
It no longer depends on which panes are still open.
The approval endpoints move next to that setter, where the rest of the
settings surface lives.
The icon drew a rounded square with a notch and a corner badge — a mark that
appears nowhere else in the product, so the tray was the one place Reasonix
was a different logo.
It is now RMark.tsx's three paths, carried over in the 16-unit space the SVG
draws them in and rasterized against a distance field: reading a stroke as a
capsule is what gives the round caps and joins without drawing either.
Mood is colour and nothing else, each hue the one meaning the palette already
assigns it — ghost is neutral, net is running on its own, accent is needs you.
The lightness belongs to neither theme, because a notification area is a
background this process cannot ask about: all three clear 3:1 against white
and against near-black.
The tests hold the two drawings together, so a path or a stroke weight that
moves in the frontend fails here instead of leaving the icon on the old glyph.
Studio never carried a SingleInstanceLock, so every double-click of the
launcher started another process over the same data home — several windows,
each binding session leases the others were already holding.
The lock is keyed to the data home rather than the executable: an installed
build, a portable one and a canary all write the same sessions, so they have
to find each other, while an explicit REASONIX_HOME stays its own instance.
The prefix is Studio's own bundle identifier, so this never answers the 1.x
desktop's lock.
REASONIX_DEV still waives it, and run-studio.sh now passes that through
`open --env` — LaunchServices does not inherit the shell's environment, so
without it a dev launch would be handed to an installed window.
Closes#9412.
`droppedRef.Image` carried `omitempty`, and false is a bool's zero value, so
the field left the JSON at precisely the value the composer needed: every file
that is not a picture answered with silence. The page read that silence as an
image — the check was `image === false`, and `undefined` is not `false` — and
drew an empty thumbnail labelled "image" over a .md file.
POST /attachments never had this: it writes its answer through a map, which
omitempty does not touch. Only a path dropped onto the window went wrong.
Both sides now state it. The field is always present, and the composer asks
whether it is an image rather than whether it was denied: a missing field falls
back to the file glyph, which still shows the name.
Two separate things were painting over a wallpaper nobody could see.
The rail: its 54px hit box is pointer-events:auto and sits over the transcript's
right edge, so any control that lays out to the right — the rewind entry is one
— lost its right half to it. The width is now a token both sides read, and the
transcript keeps clear of that column instead of fighting it for z-index, which
would only have traded a dead button for a dead stretch of track.
The wallpaper: three background rules were unconditional, and rail + panes +
side tile the window exactly, so the image was sealed under them. Two rules
written for it had therefore never once fired — .app cleared a path nothing
took, and .flow's 88% scrim, meant to catch prose over a photo, sat behind a
panel that was never transparent. With the panes clear they finally show, and
what they do is not what was wanted: the rails are down to a thin frosting,
the empty-page halo no longer tints itself from --surface (which reads as a
black fog in the dark theme), and prose carries its own shadow instead of
cutting a slab out of the picture.
Closing the metrics rail did not survive the window: nothing wrote the choice
down, so every launch opened it again. Two things had to change together —
the fold-away helper kept the wide-window value in a ref seeded to `true`, so
even within one session, narrowing the window and widening it back reopened a
rail the reader had shut.
The rail still opens by default. What is remembered is the reader closing it,
and only a deliberate close is written: folding away because the window ran out
of room leaves the wide-window choice alone.
The window process does not act on SIGTERM, so the pkill before `open` matched
the old instance and then left it running. Every `make studio` opened another
copy on top of the last, and they accumulated for as long as the machine stayed
up — two were live here, one of them a day old, before this was noticed.
Ask with SIGTERM, wait a beat, then insist.
Ranks say which column a node sits in and nothing about its row, so the
order the deltas arrived in decided it. Wires crossed for no reason a
reader could act on, an edge spanning a column was drawn straight over
whatever cards stood in the middle, and the same run redrawn came out
different.
Layout moves to its own module so the arithmetic can be checked without
rendering anything, and gains Sugiyama's two missing steps:
- Ordering. Each column is swept by the median of its neighbours' rows,
and only a sweep that lowered the crossing count is kept, so a round
cannot make the picture worse.
- Waypoints. A long edge reserves a thin row in every rank it crosses,
which is what keeps it off the cards it flies over.
- Columns are centred against the tallest rather than hung from the top,
where a lone node read as a step that ran early instead of one that
ran alone.
The panel also reads the kernel's new facts: a queued node says so
instead of sharing "not started" with a node nothing has reached yet,
the summary counts the two waits apart, and the detail strip carries
slot wait beside run time.
Folding is indexed rather than scanned. The graph is the one piece of
session state that only ever grows, so a findIndex per node and a scan
per edge made the cost of one delta the size of the whole run.
The graph declared StartedAt and EndedAt and no producer ever filled one,
so every consumer's timing fallback was dead code and a run's own order
survived only as the order its deltas happened to be published in.
Waiting for a slot and holding one were the same word besides. A fan-out
the session concurrency ceiling was throttling drew identically to one
where every item started at once — and which of the two it is decides
whether raising the ceiling would buy anything.
- agentgraph gains StateQueued and Node.QueuedAt. Terminal() had to learn
the new state: a queued node reported as settled leaves a reader no
longer waiting for the answer it is still going to get.
- SchedulerPolicy.OnStart fires once the slot is held, which is the only
moment the wait can be read from. acquireSlot moves to its own file and
becomes the single place that reports it, for both the foreground and
the background path.
- fleet and parallel_tasks publish queued, then running, then settled.
An item now settles the moment it settles rather than when its group
ends: a long fan-out used to draw every finished worker as still
running until its slowest sibling returned, which is the whole time
anyone is watching it.
Reported as Studio never raising the ask card: the model kept answering with
"ask 无交互响应(fallback)" and telling the user to go fix it. It was reading
its own tool result back. A subagent's ask found no asker and returned the
headless fallback - the text written for autonomous runs, which says in as many
words that no interactive user answered.
The root executor has an asker: EnableInteractiveApproval wires the controller
in as one, and every interactive frontend calls it. A child never did.
newAgentServices binds each collaborator from Options and there is no Asker
among them, so SetAsker is the only way in and only the controller ever calls
it - on the executor and the planner it holds, never on anything they spawn.
The gap was silent because the fallback is a plausible answer rather than an
error.
The call context already carries the right answer and nobody was reading it.
RunSubAgentWithSession is the one construction boundary every child goes
through, and by the time it runs, the context says whether a question can be
put to anyone: a delegated run has a parent blocked on it and a person waiting,
so exactly one question can be outstanding; fleet items and parallel tasks
build their context with the asker dropped on purpose, because a question from
one of many has no reader who knows which asked it; a background job runs under
a job context with no call on it at all. Inheriting from the context therefore
needs no flag and no policy - the three cases already differ where it matters.
Both halves are tests now, because "silently answers itself" is what this class
of bug looks like and neither half fails loudly. The construction moved to its
own file rather than growing task.go, which is 1200 lines past its budget.
Reported as connecting to a Linux server failing with "这次请求没能送到内核
(HTTP 502)" and nothing else. The request had arrived. The kernel had
answered, at length, and two layers threw that answer away between it and the
window.
Opening a pane over there is two hops: attach the link, then ask the kernel now
running on the far side to open a runtime. The link half already codes its
failures - a changed host key is not a network hiccup, and identify() in the
desktop says so. The second half did not. farRequest returned fmt.Errorf with
the far side's status and body flattened into a sentence, writeErr found no
code on it and fell through to http.Error, and a sentence is exactly what the
repo's own rule says an error a caller must tell apart may not be. A dead link
and a kernel that answered with a refusal are different problems with different
next moves; only a code carries that.
They have codes now. A transport failure is remote.unreachable, the same
condition the proxy beneath already reports through a different door. A far
kernel that answered is remote.kernel_refused, carrying the host and whatever
it actually said - falling back to the status line only when the body was
empty. Three writeErr call sites pick both up without changing.
The other half is the hub port, which asked for JSON and discarded anything
that was not. SseHttp was fixed for this once - users reported
"/providers/edit: 500" - and the hub was left reading res.json(), so every
plain-text answer from every hub route reached the window as a status and a
path, which reason() then renders as the request never having been sent. It
reads the body as text once and parses optionally, the way its sibling does.
Only a genuinely empty body may now claim nothing came back.
Reported as MCP not connecting automatically: twelve enabled servers, each
listing the tools it brought, every one of them reading "not connected" with a
Reconnect button beside it. Nothing was broken. That is what a healthy install
looks like, and the panel had no word for it.
Enabled config MCP is catalog-first by design. Boot registers each server's
cached toolset as placeholders and starts no process; the first real tool call
drives the handshake, and a fast server completes it in the same turn. So
host.Servers() is empty for all of them, every row falls through to the branch
for servers with no live connection, and that branch had two answers - off, or
idle - for three situations. The comment there already claimed to tell "switched
off" from "lazy and not needed yet"; the code did not.
The catalog answers it, not the host. A server whose tools are in this
session's registry can be called right now, whatever its process is doing, and
that is the question a status row is actually asking. MCPCatalogTools counts
the registry's MCP bindings in one pass - lazy placeholders included, because
they carry the server name like any other binding - and configuredState turns
that into standby, idle or disabled.
The panel says 待命 · 首次调用时启动 for standby and offers 立即连接 rather than
重连: nothing failed, so nothing is being retried. The mock's cache-hit fixture
moves with it, or the harness would keep showing the state this fixes.
Reported as storage settings reset to C: after upgrading Studio, with the
original location then impossible to choose again because it "needs an empty
folder". Two defects, and the second is the one that traps a person: whatever
loses the pointer, the data is still sitting on the other drive and there is no
way to say so.
The reset is the installer. studio.nsi asks for admin and the finish page runs
Studio itself, so the app inherits the installer's token; on an account that
elevated by entering someone else's credentials, %APPDATA% then resolves to
that administrator's profile and the first launch after an update reads a
config.toml that never heard of the user's relocation. It launches through
explorer.exe now, which is already running as the signed-in user. The exe
manifest is asInvoker, so only that one launch was ever affected - which is
exactly the launch the report describes.
The trap is that a relocation was recorded in exactly one place. A directory
now says what it is: a move writes .reasonix-root.json once the copy is proved
and before the configuration is told, so a marker means this folder is
complete, and it is removed from a location the root leaves. A preflight whose
target already holds this root's own data plans an adopt instead of a refusal -
the pointer alone, nothing copied, verified or reclaimed, and nothing at the
old location deleted. The plan reports what stays behind rather than letting it
go quietly missing.
Structure decides, never resemblance: the marker names the root, or - for a
root that declares what it owns - the folder holds those entries and nothing
else. A root with sole occupancy and no marker cannot be told from a stranger's
folder, so it is refused rather than guessed at. A folder left with only the
marker of a root that moved on is an empty target, not data to adopt.
RecordRelocatedRoots marks the locations a configuration already chose, so a
folder an older build moved data into becomes adoptable after one launch; it
follows the importers' rule and does nothing when the environment redirected
this run's roots.
While in there: PlanMove measured the whole source directory, so a state root
sharing its folder with home counted config.toml and the credentials into a
move that never copies them, and the progress bar could not reach the end it
was measured against.
Clicking a node showed what the kernel knew about it and stopped there. What a
reader wants next is the call itself — its arguments, its output, the reasoning
around it — and the transcript had no way to be pointed at one. Only user
messages carried an anchor in the DOM, because only the rail had ever needed to
find something.
Tool cards now carry the id the graph names them by, nested children included,
which is where a fan-out's nodes actually live. Nothing else reads it, so losing
it would fail silently — the button would switch tabs and do nothing — and that
is what the new test is for.
The rail's two-pass landing becomes one routine both callers share: scroll to
the block first, because the card may sit in one that is not mounted and has no
node to scroll to, then put the element itself under the reader and flash it.
The rail lands on a message, the graph lands on a call; same passes, different
selector.
The request is a counter, not an id, or asking for the same node twice would
land once. It is held rather than spent while the pane is hidden: a hidden pane
has no geometry, every rect reads zero, and the scroll goes nowhere. That also
makes the caller's ordering harmless — the view transition defers what it is
given, so setting the tab and the request in one commit is right, and holding
the request is what makes it right even when it is not.
The panel counted delegates. It could say four were running and never which of
them was blocked on which, because the list it was drawn from had already
thrown that away — the kernel now publishes the structure, so there is finally
something to draw.
A rank is a column: everything in it was free to run at that moment. Width is
concurrency and length is a chain, which is the difference a list renders
identically. Ranks are memoised with a separate in-flight set rather than one
visited set, or a node reached twice through a diamond reads as a root and the
whole picture flattens.
Edges are told apart by line, not by colour. Solid with a filled head means the
upstream answer went into the dependent's context; dashed with an open head
means the pair was ordered and nothing travelled — which is exactly what the
measurement arm produces, and drawing them the same way would say the two runs
were the same run. Reuse is a mark on the node, because an adopted item runs
nothing at all and would otherwise be indistinguishable from work that was paid
for. The state is a word on every card as well as a colour, so none of this
depends on telling hues apart.
The two questions a list cannot answer are the ones the cards lead with: a
blocked node says what it is waiting for by name, and the header says how many
of the fan-out actually ran. A separator that a node id could contain is a bug
waiting for an id with a slash in it, so pairs are keyed by encoding rather than
by joining.
The structure was being re-derived in five places and survived in none. A
fleet's dependency DAG lived and died inside one Execute call — the dispatch it
emitted carried a prompt and a profile, so depends_on never left the process at
all. The persisted sidecar recorded a parent link and nothing else. The event
stream implied the rest through id prefixes. By the time any of it reached a
reader it was a flat list of children, which is why a panel could count
delegates and never say which of them was waiting on which.
internal/agentgraph is the vocabulary, and it is a leaf: nodes, and edges that
carry an identity rather than leaving a reader to match words. depends and
context are two edges on purpose. The measurement arm orders its endpoints
while delivering nothing between them, so a single edge type would describe an
ablated run and a real one identically — the same reason an error a caller must
tell apart carries a sentinel rather than a sentence.
The other half of that rule was already being broken on disk.
InheritedContext.Upstream was a bool: it recorded that some dependency's answer
opened a run, and a reader asking which one had nowhere to look. It names them
now, and a sidecar written while it was a flag still decodes to "there were
sources, unnamed" rather than to nothing.
fleet and parallel_tasks publish what preflight already proved, before any of
it runs — an item that never executes emits no tool events to infer a state
from, and an adopted answer emits none at all. That collapses three copies of
the same five-state vocabulary into one and two identical group classifiers
into one, so a group's progress card and its graph node can no longer disagree
about how the fan-out ended.
A node is the call it names. Timing and output stay on the tool stream and a
frontend joins them by id, rather than the kernel publishing the same clock
twice under a second naming scheme. internal/boot asserts that at the frontend
sink through the real Build assembly: a scripted two-item fleet with a
dependency in it, and the edge has to arrive.
Studio could already say something mid-turn — the composer routes it through
steer, and the row on screen carries the receipt it can be taken back by. What
it could not do is see the queue. The pending rows are minted by the client on
its way out, so they say only what this window sent: reload the page and they
are gone while the entries are still in the manifest, still due at the next tool
boundary. A line the CLI queued, or a second window, was never on screen at all.
So the kernel had ten inbox endpoints and the frontend read three of them.
Editing, reordering, retrying a blocked entry, re-freezing the files one quoted,
holding delivery — all of it existed, with no way in. The panel above the
composer is that way in.
The queue arrives the way the workspace tree and the checkpoint list already do:
an event says it moved, and the content is read back whole. sessioninbox.Store
has had OnChange since it was written and nobody had ever registered one — the
notify path ran into an empty listener slice. Registering it there rather than
at each mutation is what makes this true of every mutation, including the ones
the kernel performs on itself when a turn consumes a steer.
InboxChanged carries nothing. It is tempting to put the snapshot on the wire and
save the round trip, and that would make every client's queue a function of the
frames it happened to receive — a reconnect that missed one leaves a panel
describing a queue that has since changed, and the manifest's checksums and blob
names would be on the wire for no reader. One authority answers instead, and a
frontend that has just reloaded is in exactly the same position as one that has
been connected all along.
adoptInboxLocked exists because there were two places opening a store, and a
listener registered in one of them is a listener half the sessions do not have.
Two smaller things the work needed.
The transport had four verbs and the update endpoint is a PATCH, so it has five.
The contract test that checks the frontend only calls paths this package serves
reads those verb names out of the source: without patch in that alternation the
new call was not unserved, it was unchecked. Confirmed by pointing it at a route
that does not exist and watching the test go red.
Editing opens on the entry's full text, not its preview. The manifest keeps a
120-rune line and nothing more — opening the box on that and saving it back is
how an instruction would quietly lose its last sentence. Measured against a
141-character entry: the row shows 121, the box gets 141.
Verified against a running kernel rather than the fixture, with the entries put
there by a second client so the panel had to have read them from somewhere:
three lines queued over HTTP appear in a window whose transcript never carried
them; an external reorder shows up without a reload; reorder, take-back and
release from inside the panel each land and the panel redraws itself; an edit
saved from the box reaches the kernel. That last one also found a defect this
could not have found any other way — an empty queue is items: null on the wire,
which is what a Go nil slice encodes to, and the panel reading it as an array
would have thrown at the moment the queue ran dry, and only then. Normalised in
the port, where one reader knows it instead of every reader having to.
Deliberately left alone: a line this window sent appears both as a pending row
in the transcript and as an entry in the panel. They are not the same statement.
The row is where it sits in the conversation; the entry is what is still owed,
and it is the one that survives a reload. Folding either into the other would
cost the case the panel exists for.
The window is what this version is: the greys came home to the product's own
hue, the activity stream became a rail you can scan instead of paragraphs on
white paper, and the menu bar got an icon that says what this machine is still
doing. Around it, the close button finally keeps the promise its config option
has been making since before this shell existed.
Two things are deliberately not in here. The status icon's own defects — a dead
Win32 pump, a crash on macOS launch — were introduced and fixed inside this
version, so no released build ever had them; the notes say what the icon is, not
what it survived. And fleet's for_each was built, measured against the call it
replaces, and removed in the same span: it never reached a surface, so there is
nothing for a reader to have used.
Studio did not start on macOS. `open` returned and the process was gone, with
nothing on screen and nothing in the terminal — the bundle path hides the
output. Run the binary directly and it is there:
*** Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason:
'NSWindow should only be instantiated on the main thread!'
-[SystrayAppDelegate applicationDidFinishLaunching:]
-[NSStatusBar _statusItemWithLength:]
tray_run_other.go carried a guess about this and flagged it as one: "macOS puts
its status item on the main thread and Wails already owns that one, so handing
the icon a thread of its own — the Windows fix — would be a crash rather than a
dead icon. Neither platform has been run from the machine this was written on."
The first half is right and the conclusion does not follow: nobody was handing
the icon a thread, and the thread it inherited was not the main one either.
Wails calls OnStartup from a goroutine, and the crash frame is m=5.
It is not the ownership conflict it looks like. registerSystray returns
immediately when the loop is external, so systray never takes NSApplication —
nativeStart only allocates a delegate and calls applicationDidFinishLaunching
on it directly, and that is where the status item gets built.
So the defect is narrow, and it is a lone exception in its own library: every
other systray entry point — setIcon, setTitle, every menu mutation — already
hops to the main thread through performSelectorOnMainThread. nativeStart is the
one that does not.
The hop happens here instead, the way this tree already does it. dock_icon
_darwin.m has the same shape for the same reason, and its comment names the same
caller: "the caller is a Wails lifecycle hook that may not be it."
Async, not sync. dispatch_sync would deadlock whenever the main thread has not
reached its runloop yet, which is exactly when a startup hook runs. Nothing
needs the completion: ready() ends by replaying the last state it was given, and
a setter that arrives before the delegate exists is a message to nil.
tray_run_other.go loses darwin from its build tag and the retired guess from its
comment.
Verified with tray at its default, the configuration that crashed: the window
comes up at 1440x900 and the status item is there, titled "Reasonix", its
tooltip reading "Reasonix Studio — 没有在跑的" — which is ready() having run,
built its menu and replayed state, all on the main thread.
The token system was never the weak half. The activity stream was: the column a
reader spends the whole session in had no material at all, while the rail beside
it has hierarchy, the metrics column has data, and the trajectory tab has a
gantt. In light theme it rendered as paragraphs on white paper — a terminal
block, the largest object on screen, carried border-left: 1px and background:
none.
The timeline was already there in structure and absent in pixels. .call has been
a 20px gutter with a stamp and a connecting line since it was written, but the
line was 1px of --hair, which on a screen is nothing. It is 2px now and takes
the category colour that tokens.css had already assigned and never spent —
neutral reads, amber writes, blue network, teal delegation — so a glance down
the gutter says what a stretch of the turn was doing. Sub-steps inside a
delegate panel keep the narrow gutter: two levels at one width is one level.
Blocks get material. .term sinks half a step by mixing --page into --surface,
one recipe for both themes because page already sits under surface in each. The
approval card lifts out of the flow on an amber wash, and only the one still
waiting: :has(.apv:not([data-sealed])) drops it the moment you answer. Amber
says exactly one thing in this vocabulary — it needs you — and it had been
reaching only as far as the buttons.
Wall-clock becomes a bar beside the number it already prints. It is horizontal,
which was not the first design: drawn down the gutter its length would be
bounded by the card's own height, so a 64s step with two lines of output either
pushes the layout around or gets silently compressed — and a meter that shrinks
when it runs out of room is a meter that lies. Across a header row there is no
such constraint. The mapping is logarithmic because the span is three orders of
magnitude, and the bars right-align against the duration, so a ragged left edge
down a column is the turn's time distribution.
A turn now starts at a visible boundary. Not a new Item type: the user card is
already the anchor the rail jumps to, and UserCard.tsx already records the
decision that turns carry no number, because the rewind entry sits on the card
itself and there is nothing to match up by eye. It gets a rule and some room.
The right column stops being nine equal blocks with nine equal separators, which
reads as an unfinished form. Group boundaries fall on cost, progress,
environment and output, and the first block of each is recognised by the
data-b it already carries — no reordering, no new attribute.
One piece of the plan is deliberately not here. Highlighting the command line
inside a terminal block was going to be .term-l:first-child, and the first line
is not the command: Term splits text on newlines, real shell output routinely
opens with output, and past 300 lines the block is not split at all so there is
no .term-l to select. Guessing a role from a position is the thing this tree
keeps retiring. Doing it properly means telling the renderer which line is the
command, which is a different change.
Second half, found by the sweep the previous commit repaired: 55 light-theme
findings, and they were the design's own rules going unenforced.
--ghost was calibrated against the wrong surface. The comment said "on the
--raised it actually appears on"; it appears on --raised once and on --page 768
times, because rail, side and chrome are all painted in it and it sits a step
darker. The soft tier stopped at 4.03. It is 53% now (4.52 on page), and the
comment says which surface and why. normal and strong already cleared it and are
untouched; dark measures clean on every surface it lands on and is untouched.
--accent is a fill and --accent-ink is the reading value — tokens.css says so,
and 32 rules used the fill as text anyway. In light it clears nothing: 4.07 on
its own wash, 3.91 on page, 3.80 on overlay, while the ink is 4.98 or better
everywhere. --warn is the same hue tier and had 13 more. Only text colours move;
borders, fills and shadows keep the base. In dark the ink is the base, so both
sweeps are light-only.
Two singletons around them. The slash menu's selected row mixes its own tint
rather than using --accent-wash — deliberately, and the comment explains it —
but at 12% the ink on it reached 4.40, so it is 9% and the row is no less
selected. And white on --accent sat at 4.4977, which is not 4.5; #A7690C to
#A6680C is one step of a hex digit, invisible, and lands it at 4.56.
Verified on the real shell against fixture data, both themes, every surface the
sweep can reach: 55 and 45 findings become 0 and 0, with truncation, spill,
offscreen and unnamed-control all empty and no runtime errors.
getComputedStyle now hands back oklch() and color-mix() verbatim, and parseC
dug the numbers out of whatever string it got. Three components that mean
lightness, chroma and hue were read as if they were r, g and b, so every
judgement compared two colours nobody had painted: 1037 findings on a dark
sweep, all of them "#01004B on #00004B", a pair that appears nowhere in the
token table.
A gate that fires on everything is worse than no gate for the same reason one
that fires on nothing is — both bury the findings that are real. This one had
gone all-red, and the 55 genuine light-theme findings underneath were
indistinguishable from the 807 it invented.
The fix is to stop parsing. Paint one pixel with the computed value and read it
back: whatever the browser resolved is what gets measured, and the probe needs
to know nothing about colour syntax it will have to keep up with. Dark drops
1037 to 45, light 807 to 55 — and those 55 were real, which is what the next
commit is about.
One other correction while in there: the root background fell through to black
when html was transparent, because a fully transparent colour still parsed to
four numbers. It is only adopted now when its alpha says something was painted.
A vision host rejects an oversized image as an unsupported *format*. Measured
against api.deepseek.com/anthropic on 2026-08-24: 8192x1000 is accepted,
8193x1000 is not, and neither is 1000x8193 — the limit is a single side, while
total pixels (8000x8000 passes) and bytes (a 12 MB PNG passes) are not bounded
at all. The message names webp, png, jpeg and gif, so the reader spends the next
hour confirming the file really is a PNG. It is. It is too wide.
Worse than the wrong question: the rejected user message stays in history and is
resent every turn, so one attachment ends the session. Nothing in the tree
stripped it and retried; ProviderErrBadRequest had no consumer at all.
compressForVision scaled on side length but forwarded the original whenever it
could not: an undecodable mime, a header it could not read, and — the one that
fires here — anything past the 50M-pixel decode limit. A long diagram is exactly
that shape, so it skipped the 1568 budget and went out full size.
The budget is now the gate. internal/visionimage owns the only judgement, and
what leaves it has both sides within MaxDim or does not leave: no vendor number
is written down anywhere, because 1568 already sits below every host's ceiling
and a declared 8192 would be one vendor's answer imposed on the rest. What
cannot be proven to fit is refused with ErrUnfit, an identity a caller can read,
and the reason names the dimensions and what to do about them.
It is a package rather than a control helper because MCP tool results are the
same hole reached from the other side — toolResultImage validated base64 and
size and never looked at dimensions — and internal/plugin cannot import control.
Both callers now ask the same function.
Around it, a turn's attachments stop being three loose values. turnImages says
what the model got, what a delegate could get, and what nothing got, which
retires orchestratedTurn.imagesResolved: a resolved turn is a non-nil pointer.
The headless entry points compose through imageRoutingPrefix like the
orchestrated one does — they bound the attachments but dropped the note, so a
model with an unreadable attachment was told nothing and went off to write its
own OCR script.
Refusing is only half of it; saying so is the other half. The user gets the
reason and the file, and the model is told an attachment exists that it will
never see, so it does not answer as though nothing was attached.
Verified against the real endpoint, one arm each side of the budget:
12000x5000 (60M px) refused locally, "past the 50000000-pixel decode
limit ... shrink it first"; the model relays it and
suggests sips -Z 7000. The turn completes.
20000x1000 (20M px) downscaled to 1568x78, sent, answered correctly.
12000x5000 sent raw 400 unsupported image — what shipped before this.
Two deliberate gaps. The 50M-pixel decode limit is unchanged, so an 8000x8000
image the host would have taken is now refused locally; raising it trades
against decode-time memory and wants its own measurement. And a session already
carrying a rejected image still needs a rewind — this stops the poisoning, it
does not clean up after it.
Test fixtures that were PNG signatures over zero bytes are now decodable PNGs:
an image whose dimensions cannot be read no longer travels, so a header-only
fixture asserted nothing.
Nothing held docs/SPEC.md against its Chinese companion, which is how 3.13 lost
a table row and 3.14 fell three commits behind before anyone noticed — long
enough for one of its sentences to become a false statement about where
dependencies travel.
Parity is declared, never inferred. The companion abridges most sections on
purpose: 3.7's 111-line list is nine lines there, and twelve of seventeen
sections differ structurally by choice. A whole-file rule would fight that and
land twelve findings of recorded debt on day one. lockedSpecSections names the
five that are full translations — 3.11 and 3.13 through 3.16, the delegation
group, which is where changes actually land — and adding a section to it commits
every later change to that section to both files. It is the same reason the
host's sensitive paths are declared rather than read out of how a path is
spelled.
The check reads structure and never wording: the heading sequence across both
files, and each locked section's sequence of block kinds. Size is compared only
where a translation cannot legitimately change it — a table row and a fenced
line survive translation, a wrapped sentence does not — so prose line counts are
never compared, English hard-wrapping at 80 columns against unwrapped Chinese
stays quiet, and a test pins that. Comparing prose would fire on every
legitimate translation choice, which is a judgement this tool cannot read from
structure.
It lands at zero recorded debt: the five locked sections are in parity as of the
previous commit, so the rule's budget is zero and any drift fails rather than
being absorbed.
Verified against the drift it was written for. Dropping the 3.13 table row that
actually went missing reports:
docs/SPEC.zh-CN.md:282: [spec-parity] section 3.13 table block 2 has 9 lines
here and 10 in docs/SPEC.md
and exits 1.
A failed item collapsed every non-cancellation error into one status plus a
sentence, so the caller deciding whether to re-issue the graph with the finished
nodes adopted had to read the message. The identity already existed one layer
down and was being discarded at the fleet boundary: provider.RetryableStatus
knows 429 from 400, and APIError.ErrorCode knows an overflow from a rejection.
Confirmed the identity survives the trip before building on it: a
*provider.APIError returned by Stream reaches RunProfileSpec intact, status and
code readable through errors.As. Classification therefore reads what the error
already carries rather than decorating it.
Four classes, each sourced from a real identity — provider.transient (408, 429,
5xx, or an interrupted stream), provider.rejected (any other 4xx),
context.exhausted (ErrCompactionRequired, or a documented context-overflow
code), and unclassified for everything else. Nothing reads message text. An
error whose producer named nothing is reported unclassified rather than guessed
at, and a test pins that an error whose message reads "rate limit exceeded"
stays unclassified: this is the rule that retired the phrase tables, and a
failure surface is not where it gets an exception.
The identity travels to the branch the failure killed, because a skipped item is
worth re-issuing exactly when its cause was.
Skip policy is unchanged. Retrying inside a run is a separate decision and needs
its own evidence; adopt_ref is already the caller-side retry primitive.
Second half, found while testing the first: a skipped branch made runFleet report
the whole call as cancelled and answer context.Canceled. A rate limit is not an
interruption, and that error is what the turn loop reads to tell an interrupted
call from one that ran and reported. Only a real cancellation answers it now —
including the pre-cancelled context the old skipped check happened to cover — and
the aggregate headline gained the third state it was missing, so a run that
reached its end with failures says how many of its tasks were answered instead of
claiming to be either clean or cancelled.
docs/SPEC.zh-CN.md had drifted three commits behind on 3.13 and 3.14: the
upstream payload, adopt_ref with list_subagents, and the measured for_each
exclusion never crossed, and one sentence had become false — dependencies "never
reach ProfileExecSpec" after they had started travelling on
ContextRequest.Upstream. Both files now describe what the kernel does.
The cycle ran a fixed ["auto", "light", "dark"], and "follow the system"
looks exactly like whichever of the two it currently resolves to. On a
light machine the default auto stepped to light: not one pixel moved, so
the control read as broken. The next step now comes from what the system
is rather than from the array's order, and the first click always
repaints.
One hop stays silent -- returning to auto lands on the scheme auto already
shows, and that is what those three states mean. The button answers the
press itself so the click is acknowledged either way.
Every neutral in the palette sat at hue 255-267 while the accent sits at
75: the greys were 185 degrees from the one colour the product owns, which
is what made them read as somebody else's. They are OKLCH now, at the
accent's hue and a chroma low enough to still be grey, with the lightness
carried over untouched from the hex each one replaces -- so every contrast
ratio the comments there argue for still holds.
The four surface steps were declared but only the centre used them, so the
window was one flat plane with hairlines drawn on it. The frame drops to
the canvas and the middle column becomes a sheet on it. Radii go to
1/2/3/4: an instrument is machined, not moulded, which is what lets rules
and columns take over the dividing. Terminal output, delegate panels and
returns, the approval, and the right column's sections all lose their
boxes for rules.
--warn and --accent were the same hex, so "each colour does one job" was
not true of those two: needing you and costing you were indistinguishable.
warn moves to H45 at 8.14:1 dark and 5.83:1 light, matching accent's 8.35
and 5.77.
The mono stack shipped no face, so the layer carrying every label, tag,
figure and terminal line rendered in Cascadia on Windows and SF Mono on a
Mac -- the problem the comment above --ui says was solved, still open one
line below it. IBM Plex Mono Var, 81KB, OFL like the other two. It has no
geometric shapes and no U+2717, which fall through to the shipped Noto, so
they stay identical across platforms but off the monospace grid; the two
places a check and a cross share a marker column use x instead.
Motion: a weight tier now decides duration and easing together, and
choreography has one mechanism -- sibling-index() rather than fifteen
hand-written nth-child rules. The composer row had grown to eight items
against a chain that stopped at seven, so the eighth entered with no delay
at all. Streamed transcript cards are deliberately not staggered: they
arrive one at a time already, and delaying them would delay the work.
Light is emitted by readings, never painted on surfaces: the live symbol
blooms in its own category colour, the gauge leaves a phosphor edge as it
grows, and a finished turn gets one sweep and one flash. A grain overlay
takes the sterility off surfaces that are otherwise perfectly uniform,
which no real material is.
event.go states the contract: a Retrying indicator is transient, and the
next stream event -- or TurnDone -- clears it. The chat TUI does that. The
window only cleared it on a text or reasoning delta, so a turn whose first
packet was a tool call left "retrying 1/5" and its clock standing for the
rest of the turn, over calls that were running fine.
The clearing moves to a rule about event kinds: the kinds that say nothing
about the connection leave the wait standing, and everything else is the
provider having answered.
Two more the same measurement turned up. RetryScope was on the wire and
ignored, so a body-phase replay was announced with the header-phase
sentence -- 1/5 is maxStreamRecoveries, and headers retry ten times. And a
retry between two steps used to show nothing at all, because the indicator
only rendered inside a wait that the first packet had already ended.
The clock now counts from the stamp the wait carries rather than from the
component's mount, so it reports the stall rather than the turn.
The prefix is what prompt caching pays for and one changed byte re-bills all of
it, so the useful question is not how often the cache held but what stopped it.
CompareShape already answers that every turn — system, tools, or the reason a
Rewrite queued — and the verdict rode to the frontend and stopped. This carries
it: cache_miss_reason, one bucket per attributed cause, nothing inferred here.
Rewrite takes an open reason string, so an unknown one buckets as other instead
of letting a caller widen the wire's bucket space.
The complement is the case the kernel cannot see. A prefix that held and still
returned no cached tokens is a gateway that dropped cache_control, an entry that
expired, an endpoint that never cached — downstream, unattributable, and exactly
the shape of the route that billed twelve million prompt tokens at full rate
with nothing anywhere reporting it. It buckets as unexplained, above the
smallest prefix any vendor here will cache so that turns which were never
cacheable stay out of it.
uploadSignals is the silent gate: a counter the sink records but the list omits
is queued and dropped without a word, and locally that is indistinguishable from
a feature that works. A guard now drives a turn through every recording branch
and fails if the wire rejects what came out. It found tool_call_reasoning_recovery
dropped since it shipped — the bucket is a ProtocolRecoveryKind, a closed enum,
never content — and that is now allowed through.
Built, shipped behind the surface, and measured against the call it replaces —
the same graph with one task handling every subject. Three fixtures, twenty-eight
graded runs against a real provider, every run of both arms solved, and mapping
spent 70%, 85% and 98% more tokens for it (exact permutation p <= 0.03 each).
The last fixture was written to be the case mapping should win: ten modules of
near-identical code, a hundred and sixty lines each, and an answer that is the
sum of ten separate derivations so a slip anywhere is a wrong answer. It did not
win there either, and one run solved it by grepping the constants out rather than
reading anything — which is the reason. A mapped child pays a full sub-agent's
fixed cost per subject, while a single child with a tool belt never has to hold
the subjects at all: it reads them one at a time, and when the facts are
extractable it extracts. "One context cannot hold N subjects" assumes an
ingestion an agent does not have to perform.
So the graph vocabulary goes back to id and depends_on, and SPEC 3.14 records the
exclusion as measured rather than as taste.
Two things survive the removal. The edge dedupe in newFleetPlan stays: a task
named twice in one depends_on was already counted twice before for_each existed,
and the item would have waited for a result nothing publishes. So does
unresolvedDepCounts, which adoption needs — an adopted node enters resolved and
its dependents have to be ready on the first pass.
benchmarks/fleet-fanout goes with it. The history holds both if a case that
genuinely needs per-subject isolation ever turns up.
Claude caches only where cache_control marks a breakpoint, and that field is
serialized by internal/provider/anthropic alone. The same model reached through
an openai-kind entry is not an error and not a visible wire difference — it is
full input rate on every prompt token, and the only symptom is a cache read
that never arrives. Measured on a live gateway: 3610 input tokens with no
breakpoint against 7 plus a 3603-token cache read with one.
The judgement reads structure and nothing else — an entry that sets both a
dialect and a model list, resolved through the package-level slice the models
usually live in, because an entry that never spells the name in its own literal
is the ordinary case. Fixtures are exempt: a test builds the broken shape on
purpose to assert what it does.
realyyds_test.go pins the reason a yyds-anthropic entry exists at all, in the
//go:build live form this repo already uses for gateway probes.
The one finding is recorded, not fixed: the vendor's Anthropic route could not
be reached to tell a gateway that lacks one from a preset that picked the wrong
dialect, and asserting either in source would be inventing the fact. The
ratchet is where an open question of that shape belongs, and the rule still
fails every new one.
The counters said how often a turn cached well and never how much it cost.
A flat-rate plan is priced on the tail, so the shape has to survive the wire:
turn_prompt_tokens, turn_output_tokens and turn_cached_tokens are magnitude
buckets taken once per turn, because three 40k requests are one 120k turn and
the plan is paid on the turn. The per-turn scalars move behind turnCounts so
both boundaries reset by whole-struct assignment and the next field added
starts clean.
Surface was hardcoded "cli" at both post sites while stats spelled the same
vocabulary a third way, so internal/surface now owns it and boot, telemetry
and serve read it from there. Or(fallback) makes a caller say what an unset
field could only have meant rather than letting the code guess. One machine's
queue is shared by every surface on it, so the flush groups by surface as well
— without that a window's turns post as the bare server's.
The window and the headless server reported nothing at all: DesktopMetrics and
DesktopTelemetry were settings with no sender. They are separate consents, so
backgroundFlush takes the ping apart from the drain and each is honored alone.
Enabled stays fail-closed, so a development build, CI or DO_NOT_TRACK still
refuses, and a non-interactive server counts only where telemetry was turned
on deliberately.
runServeWithOptions moves to serve_frontend.go, where the serve frontend's
entry point belongs; cli.go was 2595 lines and 1795 over the ceiling, and the
extraction lowers its budget by more than the move costs the file that gains it.
Two fixtures, twenty graded runs against a real provider, and mapping never once
solved something the single-task form did not: 20/20 on both arms. What it cost
was 85% more tokens on the cheap fixture (exact permutation p=0.002) and 98% on
the one written to favour it — ten modules each hiding its effective timeout
behind a three-step derivation, where grepping the constants answers nothing
(p=0.029) — with no wall-clock gain either time, at the default concurrency and
at a ceiling raised past the mapped width.
So the mapped fields leave the provider-visible surface. The schema field and the
sentence describing it are dropped together, so the description never advertises
a parameter the schema does not carry, and a call naming for_each is refused
rather than half-honoured.
There is no setting. A capability the host cannot recommend does not get a knob:
turning it on is a change at the wiring point, to be made once the case that is
still untested is measured — subjects whose combined content one child cannot
hold at all, which neither fixture builds.
The code and its tests stay, and benchmarks/fleet-fanout now holds both pairs.
The evidence for switching it back on and the evidence for deleting it is the
same suite either way.
A capability has no ablation arm — there is no "the same thing with the payload
removed" for for_each the way the dependency edge has one — so the honest
comparison is what this call had to look like before it existed: one task
handling every subject. benchmarks/fleet-fanout holds both, identical except
that the middle item takes for_each rather than depends_on, graded the same.
Against the real provider, n=6 per arm across two concurrency ceilings, both
arms solved 6/6, and mapping a one-file check over twelve subjects spent 85%
more tokens (exact permutation p=0.002) and 47% more money (p=0.026) for no
wall-clock difference (p=0.70). Raising max_subagent_concurrency past the width
did cut the mapped arm's wall time, and cut the single task's by more.
The mechanism is a sub-agent's fixed cost — prefix, reasoning, closing contract,
once per subject — against per-subject work of reading one short file. It is the
same boundary the dependency edge has: both pay in proportion to what a unit of
work costs relative to that overhead, and both fixtures so far measured the cheap
side. So the tool description now says to map only work that needs a run of its
own, and SPEC 3.14 records what the fixture measured and what it did not.
for_each names the item whose submitted subjects this item maps over: its prompt
becomes a template run once per subject, and it reports one bounded aggregate
that its dependents open with over the ordinary dependency edge.
The mapped children are never vertices. The node stays one, so the reachability,
write claims, and skip rules preflight proved are the ones that ran — adding N
vertices mid-run would invalidate the precomputed reachability that
skipDependents and the concurrent-claim check both read.
Four things are fixed before anything starts, and that is what keeps for_each
from being the first keyword of a workflow language: the template, the width
ceiling (max_items, defaulted to 8 and hard-capped at 32), the read-only grant —
which removes the write-claim analysis the host could not have done for an
unknown number of writers — and the source. The source supplies only which
subjects, through submit_items rather than prose, because reading a list out of
an answer is the host guessing at a judgement the structure does not carry. The
caller owns the topology, the worker owns the data: the same split the
dependency edge makes.
submit_items reaches only a source's registry, so a capability nothing in a run
can use never enters its prompt. A source that finishes without submitting fails
the mapped node rather than quietly mapping nothing, while an empty submission is
a real answer that completes it. A source may not be adopted, having never run to
submit anything, nor itself mapped, since it answers with an aggregate rather
than a list.
SPEC 3.14 said the graph vocabulary had no dynamic fan-out. It now says no
computed edges, which is the invariant that actually mattered.
Execute's preflight moved into preflight(), which returns a proven fleetCall,
and the ctx-gated tool attach sits beside the other ctx-keyed host framing in the
two sub-session entries; that is what paid for the new lines without widening the
repolint baseline.
depends_on ordered its endpoints and delivered nothing, so a research →
implement pair paid for the same research twice. Three changes that compose:
- The edge carries the dependency's answer. It travels as
ContextRequest.Upstream, never inside TaskSpec.Objective, because the
objective must stay the pristine text delivery classification judges —
ClassifierTaskText is captured before any host framing. One 8 KiB budget is
shared across a node's dependencies, so a wide fan-in cannot flood the
opening turn and no dependency is starved out of it.
- list_subagents enumerates the children a conversation owns. A reference is
the only address a child answer has, and a background fleet that a restart
repaired to interrupted never delivered the aggregate carrying those
references, leaving its completed children unreachable.
- adopt_ref stands a node down to an already-completed child's answer instead
of running it. It is proven in preflight through ReadFinalAnswer, so what may
be adopted cannot drift from what read_subagent_result may read. Nothing
runs, so the node claims no write_paths and cannot collide with a live
writer; prompt and every execution field are refused on it rather than
silently ignored.
Whether an adopted answer is still valid is not readable from structure, so the
host does not decide it: adoption is addressed by reference, and the caller that
listed the reference judges freshness. That is also why a plan fingerprint does
not quietly reuse matching children — reuse the caller cannot see is reuse it
cannot correct.
A child now inherits something, which is exactly what InheritedContext exists to
keep a decision rather than an accident, so the capsule records it and SPEC 3.13
says so.
--ablate upstream leaves the edge ordering its endpoints while delivering
nothing, which is what lets the payload be measured apart from the order.
Against the real provider on benchmarks/upstream-edge/fleet-edge-wide, n=6 per
arm, removing the payload cost +27.6% tokens (exact permutation p=0.011) and
+59% sub-agent tool calls for the same answer, 6/6 solved either way. On a fact
one grep away it buys nothing: the edge pays in proportion to what the
upstream's work cost against how small its result is.
boot.go's delegation registrations moved to delegation_tools.go and Execute's
spec loop to buildItemSpecs; that, plus folding two defaulting branches into
firstNonEmpty, is what paid for the new lines without widening the repolint
baseline.
The refusal carried "use POST /inbox/items for durable follow-up" as its
detail, and the frontend's wording for that code interpolates the detail — so
what reached the screen was half a Chinese sentence with an API route glued to
it. The kernel was telling the client what to do and the client was showing
that instruction to a person.
Both halves. The refusal keeps its code and drops the route: the English is for
a log now. And the client does what the code asks — it queues the words as a
durable follow-up, which is a row that says it is waiting rather than an error
nobody can act on. That also closes the race behind the report: the turn reads
as finished on screen while the kernel is still landing it, and typing in that
window produced a refusal for timing, not for content.
A queued row now says which wait it is in. Guidance lands at the next tool
boundary; a follow-up goes when the turn does, and the two are not the same
promise.
DetectLanguage reads REASONIX_LANG, then the POSIX locale variables. Windows
sets none of them, so every native surface on a Chinese Windows fell back to
English — the CLI's own words, the folder picker, the status icon's menu —
while the webview beside them read the browser's list and got it right. Two
halves of one app answering the same question differently.
The machine is asked last, after anything set on purpose. It goes through a
variable so the priority between the two is testable on a machine that answers
either way, rather than only on one that happens to be installed in Chinese.
Catalog could not use it: an empty preference means "follow the machine", and
what Catalog returned instead was whatever the process had already installed —
English, for a window that installs nothing. CatalogFor resolves the same way
DetectLanguage does and installs nothing, which is what a shell whose interface
language is a setting of its own actually needs.
Three defects, one feature. The icon drew and then answered nothing, left
button or right: systray creates its window and pumps GetMessage on whichever
goroutine calls Run, and RunWithExternalLoop — which reads like the embedding
path — starts that pump on a goroutine nobody pinned. A Win32 message queue
belongs to a thread, so the window sat on one and the pump read another. It has
its own locked thread now, on Windows; the other platforms keep the documented
path, because macOS puts its status item on the main thread and Wails already
owns that one.
Clicking it did nothing for the commonest case, a window that was open and
merely buried: show and unminimise are both no-ops on that, so the raise has to
be asked for. And the icon read as a brown blob with a black slit, because the
notch was painted in the window's own dark page colour — an assumption about
the taskbar behind it that a light one disproves. It is cut out now.
The switch for it could not be switched: the field took the value and the
renderer never wrote it, so every save dropped it and the setting turned itself
back on. A setting the renderer drops is a setting that does not exist, and the
round trip is now a test.
The settings pane draws the dependency instead of enforcing it invisibly: the
close-to-tray switch hangs off the icon switch, indented, because without an
icon there is nothing to bring a hidden window back and the second switch is
not a choice. The icon itself is a launch preference — systray quits once per
process, so an immediate off would be a switch that cannot be switched back.
[desktop] close_behavior has been normalized, migrated and editable since
before this shell existed, and read by nobody: quit|background was a promise
the app did not keep. This is the half that was missing. The close button hides
where an icon can bring the window back, and quits where it cannot - no icon,
no backgrounding, because a window that vanished with no way back to it would
be worse than either.
What the icon says is folded in internal/traystate, which owns no OS handle and
draws nothing: panes in, one State out, ranked so that being needed outranks
being busy. Nothing on the wire announces an answer to an approval, so the fold
reads the first thing the run produces afterwards as the resolution - a turn
blocked on one produces nothing until it is answered, which is what makes that
sound rather than a guess. Background jobs are pulled on a timer instead,
because a job announces its life in a sentence and matching those words is the
judgement this codebase keeps retiring.
Quitting from the menu runs the window's own close sequence rather than a
second one. The reason that sequence exists - two processes writing one
transcript - does not care which control asked for it.
Windows is verified: it builds, and the icon's own encoder is the 22-byte ICO
header around a PNG. macOS and Linux compile from here but cannot run: this
machine has no CGO toolchain for either, and the thing to check there is
whether systray's external loop is happy beside Wails' own on the main thread.
/config/problem and /config/repair reached the kernel over HTTP and nowhere
else: the window routes by table, so both answered with index.html, and the
banner that says a config file will not parse could never load in Studio. The
dev proxy had the same hole.
The two guards that read the kernel's own registrations are what caught it,
which is the whole reason they read those rather than the client.
Typing while a turn runs queues the line for the next tool boundary, and nothing
could cancel it. The durable item had an id and a delete route, but the delete
refused the one state a queued steer is in, and the frontend had thrown the
receipt away - so the row on screen had no name to cancel by.
The authorization comes from the executor's queue rather than the durable state.
Its lock is the only place that can tell "not read yet" from "already on its way
to the model" without racing the run loop, so the drop there is what permits
removing the durable half: in that order, and not at all if the drop fails. Too
late answers with its own identity instead of a state name, because "removed"
and "already gone" are different sentences to the person waiting.
It rides the verb that already existed, so the CLI's inbox pane, ACP and the bot
inherit it with the controller. Studio grows one control beside the queued badge,
visible without hovering for it.
The settings screen showed built-in defaults while the user's own config sat
unreadable on disk - settings nobody had chosen, presented as theirs - and said
nothing until a save was refused with a parser's sentence about columns. The
runtime already recovers from this by loading the last-known-good snapshot;
loadForEdit does the same now. The save guard is unchanged, because a file
nothing has read is not a file to overwrite.
The refusal carries an identity. A config that will not parse is one condition
wherever it surfaces, so every panel answers config.unparsed with the line, its
text, and the one repair that is certain: a Windows path in a basic string, said
the way TOML accepts. The parser locates the escape it refused and the parser
confirms the result, so nothing here reads the message it wrote and a file it
cannot finish is left alone. The sandbox explains itself by code for the same
reason - the English under a Chinese heading was the kernel talking to a person.
While that path was open: the 24 places in internal/serve answering with a bare
error string now send a code, and refusal-path guards that envelope the way it
already guarded http.Error. A body carrying other fields is a report a panel
renders, and stays out of it.
A background job held the workspace writer lease until it was terminal, and for
a dev server that is never: the next session's first write waited behind
something that was never going to end, told only that it would continue
automatically when it was safe. The retention hook fired for every registered
job, so the case it really covered - a process the user left running - is the
one case it must not.
Agent runs already take the lease for themselves, so nothing needs the hook. It
is gone, with RetainUntil and the jobs option that carried it. A background
subagent still holds the lease for its whole run, because that run is bounded;
a process left behind is not.
The wait it does report is legible now. Contention under a second stays silent -
it is either milliseconds or a whole turn, and reporting the first kind left a
permanent line about a wait nobody experienced. Past that the open is a warning,
because the turn is stopped for the length of it, and it is always closed by a
report carrying the measured wait. Studio holds the state on the run strip while
it lasts and rewrites the card in place when it ends; a headless run sees both
ends on stderr, where it used to see nothing at all.
Remote workspaces are what this version is: a machine reached over SSH whose
folders and conversations sit in the sidebar beside the local ones. The rest
is what that took — a Windows host, a fingerprint someone has to read, a pane
addressing a runtime of its own over there — plus the effort ladder becoming
a fact about the endpoint, which is the one entry that changes what an
existing setup does.
`until_output_matches` and the reworded timeout_seconds description are a
deliberate change to the provider-visible tool surface, so the recorded
baseline moves with them.
Cache impact: the tools hash changes and the prefix hash with it, which
cold-starts the prompt cache once per machine. The system prompt is
untouched, and the schema grows by 91 tokens.
Polling a long job costs a round per look, and the alternative was waiting
blind — so the model kept choosing to poll. wait now takes a pattern and
returns the moment a job writes something matching it: one round for a job
that runs an hour, and still woken by the line that matters.
The pattern comes from the caller, because only the caller knows what it is
waiting for. A host deciding which words deserve an interruption would be the
same guess this codebase keeps retiring, and RE2 makes an untrusted pattern
linear rather than a way to hang the run.
Two things a naive version gets wrong, both found by the tests failing:
a watch registered after the line was written must still match — otherwise
asking a moment too late waits out the whole timeout for something already
true — and one registered mid-line must be seeded from what is already there,
or a pattern the next write completes is never seen.
What a wait reports is a reading, not a verdict: how long the job has run,
everything it has produced, and how much of that arrived while this call was
waiting. Slow progress and a stop differ there, and which one a task should
look like is the caller's to know — not a threshold anybody has to configure.
Two CI failures, both mine. Windows lint: the agent method was offered
unconditionally there, since the pipe name is fixed and nothing read an
environment variable to decide — so the caller's nil check could never be
false. It knocks first now, which is both what the check wanted and one less
dial per handshake on a machine with no agent. Windows tests: three of them
built a JSON body by hand around t.TempDir(), and every backslash in a
Windows path is an escape, so the body never parsed.
And the report behind it: a background job that redirects its output into a
file is silent on its own streams, which the watchdog called "may be stalled"
after fifteen minutes of it downloading a hundred megabytes. Silence is the
fact; stalled was the guess. wait now says how long a job has been running
and whether any of it ever reached this side, so the difference between a
task writing elsewhere and a hung one is visible rather than inferred — and
the answer names the longer timeout instead of another short step.
Two halves of one report from a user, whose screen showed the same notice
twice and then "model finished without a visible final answer 3 times".
The cause: a gateway can answer 200, hold the stream open long enough to look
healthy, and end it having sent nothing — no content, no reasoning, no tool
call, not even a usage record. That reached the turn as an empty answer, so it
spent three retries on it and named the wrong party. Told apart, it is
StreamInterruptUpstreamError like any other failure the far side reports
inside a 200: retried by the request layer, and ending with what happened. A
stream carrying only a usage record is still the model answering, and keeps
the turn's retry — the case that guard exists for.
The screen: repeats fold into one line and a count. Notices carry a code on
the wire that the session state dropped, and with it the ones this build has
wording for read in the reader's language rather than the kernel's English.
A code it does not know still reads.
Classification moved into stream_failure.go, which is what that file is for.
openai.go got shorter for it and the freed budget is reclaimed.
A machine worked on for months holds thousands of conversations. The local
column caps its list at thirty for that reason — drawing them all is what put
98k nodes in a sidebar once — and the remote tree, added later, drew every one
of them. A host can be folded now too, which is what makes several of them a
list rather than a scroll with the local projects somewhere above it.
Two things followed the focused pane that should not have.
The event pump still asked the far hub for /events with no runtime prefix, so
it listened to that hub's default runtime while the pane sent its turns to the
one it had opened. Everything worked and nothing came back.
Appearance was read and written through whichever pane had focus. A remote
kernel keeps its own copy, so switching to a pane on another machine repainted
this window with that machine's theme. It belongs to the window: the local
pane is what these three now read and write.
Windows answers SFTP with /C:/Users/... and its kernel takes C:\Users\... The
first spelling travelled all the way to the far hub, which tried to open
C:\C:\Users\... and refused — reaching the window as a bare 502, because that
one path had no code on it.
The platform converts, since only it knows both. The pane's name is cut the
same way: either separator can appear in a path this machine does not hold,
and its own filepath rules are not the authority on one.
It never was. The launcher linked the frontend in from the source tree, and a
symlink whose destination is outside the bundle fails codesign --strict — so
the app had a signature that verified nothing, and an app macOS cannot verify
gets no stable identity to hang a Local Network grant on. That is the whole
distance between "ping reaches the host" and the window saying no route to it.
Both fixes are the released bundle's own: copy dist rather than link the tree
(scripts/studio-build.sh has always copied), and sign --deep, because
everything under Contents/MacOS counts as code and each file needs its own
signature. The launcher now verifies afterwards and says so if it fails,
rather than leaving that to be discovered as a networking bug.
Three places still told the reader that a remote must be Linux or macOS, the
window's refusal among them. What it should say is narrower and more useful:
the link is fine, that machine's system is what a kernel cannot be installed
onto — and name the three that can.
Everything below the SSH link assumed a POSIX shell. It now goes through one
interface — which shell, which spelling of a path, how a process is started
there and found again — so the difference is in one place instead of a branch
at every call site.
Two things about Windows had to be discovered against a real machine rather
than reasoned about:
Detachment. OpenSSH puts a session's children in a job object and ends them
with the session, so a serve started by Start-Process was gone before its
port file appeared. What the WMI service creates has no such parent, and that
is the setsid of this platform.
Quoting. Commands travel as -EncodedCommand — base64 of UTF-16LE, which holds
no character cmd treats as special. That removes the outer quoting problem
rather than solving it: cmd's rules are where this would have become a
security bug, and a path with an apostrophe in it now proves the point in a
test. Inside the script PowerShell's single quotes follow the POSIX rule.
Also: the CLI release ships Windows as a zip holding reasonix.exe, and the
downloader knew only tar.gz holding reasonix — so a Windows remote had no
install path at all.
Verified end to end against a real Windows host: bootstrap, launch, survive
the session, HTTP through the forward to /status and /tree, then stop.
macOS grants Local Network access only to an app whose Info.plist says what
it wants it for. Neither bundle said anything, so the request was never put
to the user — no prompt, no entry in the privacy list, and every LAN host
answering "no route to host" while ping and the CLI reached it fine.
The release bundle had this too: any host on the user's own network was
unreachable from the window and reachable from everything else.
The launcher also signs with a development certificate where one exists. An
ad-hoc signature's hash changes with every build, so the grant would have to
be given again after each one, which is indistinguishable from it not working.
LocalBinary was copied from the CLI, where the running executable is reasonix
and uploading it is the fast path. Here it is the window: the far side would
receive a Wails app, probe it for a serve command, and reject it — after
paying for the transfer. Left unset, the install falls to npm and then to a
verified release download, which is what it should have done from the start.
Connecting to a Windows host failed as a bare 502 — "the request did not
reach the kernel" — when the SSH link was fine and the far end simply has no
uname. The reason existed the whole way up and was dropped at the last hop:
an error with no code reaches the window as a status and nothing else.
Unsupported remotes now carry a sentinel, so the window can say which half
failed rather than implying the link did. The wording carries no detail on
purpose: what such a machine answers is its own shell's complaint in its own
code page, and putting that on screen explains nothing.
Everything else keeps its text under a coded fallback, which is what makes a
failure legible at all instead of arriving as a number.
Everything above the dial — what the agent offers, which keys IdentitiesOnly
filters out — was inside the !windows build tag, so the Windows half could
not have reused it even when someone wrote it. It moves to a file with no
tag; each platform now supplies only how to reach the agent.
Windows reaches it through the pipe its own OpenSSH service fixes the name
of, with SSH_AUTH_SOCK winning where a port of the Unix agent set one. A
named pipe opens as a file, which is the same call the agent's own clients
make, so this needs nothing new to depend on.
The tests are the first for this code that run on every platform: an agent
key authenticating, IdentitiesOnly offering one key out of two, and an absent
agent not consuming publickey — the last mattering most where the pipe name
is fixed and the method is therefore always registered.
Until now a remote host offered exactly one workspace — whichever the book
named — which made it a shortcut rather than a machine you can work on. A
connected one now answers for itself: its folders and the conversations in
them, read through a pane already open on it, so no second connection and no
second serve process.
That is also why a pane had to address its own runtime over there first. With
one, resuming a remote conversation is the same act as resuming a local one,
and the sidebar can tell which of them this window is already driving.
Unconnected hosts keep the single row, which is also the connect button:
before a kernel is running there is nothing on that machine to ask.
The proxy strips the pane's prefix, so what reached the far hub named no
runtime and landed on its default. Two panes on one workspace share a link
and an address by design — and were sharing the conversation as well: one
transcript, two writers, both streams interleaved.
A pane now opens its own runtime on the far side and addresses it, which is
also what makes resuming a remote session possible at all. It is retired when
the pane closes, before the forward that reaches it goes away, or the far
machine collects runtimes until its own ceiling refuses the next connect.
The kernel builds a structured mismatch — which record it contradicts, down
to the line — and the path to the window flattened it into a sentence. Now
the link layer names what it knows: a changed key, a declined fingerprint and
a refused credential each carry their own code, and the mismatch carries the
known_hosts record so the reader can go look. There is still no way from
there to connecting anyway; a warning you can click past is not one.
The parity guard did not catch the missing wording, because it matched bare
identifiers only and a host outside the package calls the constructor
qualified. It watches both now — verified by removing a wording and seeing it
fail, which is the only way to know a guard is looking at anything.
Until now the link layer had nobody to ask, so it took the strict path: a
machine not already in known_hosts was refused, and a passphrase-locked key
could not be used at all. Both are questions, and the window is the only
thing that can put one to a person — so they travel out on the shell's bus
and the answer comes back through a binding, since the dial is blocked on it.
The fingerprint dialog is deliberately the harder one to get past. Its
fingerprint wraps rather than truncates, because one with its middle elided
cannot be compared to anything, and the focus starts on the way out: Enter is
a reflex, and accepting a key nobody read is what this exists to prevent.
A machine was only addable from the CLI, which made the sidebar section
something the window could show but never fill. The book is user-global, so
an entry belongs to the person and not to whichever folder happened to be
open when it was added.
The listing returns the stored row in full rather than the fields a row
displays: saving replaces an entry, so a page editing one field while holding
a partial copy would blank the passphrase variable somebody set from the CLI.
Forwards have no control here and are carried through for the same reason.
Importing beats typing — on a machine that already uses ssh the addresses are
written down next door, so ~/.ssh/config aliases the book does not have yet
are offered as one-click rows.
Hosts are a section, not a prefix: workspaces on one machine share a
connection, so the state belongs on the group — when a link drops, the whole
section dims together, which is what those rows actually are to each other.
Local workspaces stay unmarked, so carrying a chip is itself the signal. The
same chip appears on the pane tab and in the crumb, because a command lands
on whichever machine the focused tab points at.
A cold connect installs a binary on the far side. The kernel already reports
each step, so the wait is a list of them rather than a spinner that cannot
tell downloading from stuck.
Two function fields become one interface. The split it draws is the one that
matters: the book of hosts is configuration and stays in serve, while what a
link is doing belongs to whoever holds it — so the hub joins them without
learning what SSH is.
The pool records state whether or not a caller subscribed. A window that
opens a second pane an hour later reads the same row as the one that dialled,
and the connect step a first attach is on has somewhere to be read from.
The hub gets the endpoint from a hook the host wires, so the two halves meet
above it: a window supplies a pool that dials, and a listening server leaves
it nil. That default is the point — a kernel that dials onward because a
request asked it to is someone else's way into the network behind it.
A hold the attach took and the hub refused would be a connection nothing can
ever close, so the endpoint gives it back on every path that does not publish
a pane.
Three sweeps reached into a local assembly and would have found none. The
stream needed more than a guard: the asset server holds a response until its
handler returns, so a page inside the window cannot read SSE at all — the
shell reads the far kernel's stream and re-emits it onto the same bus a local
pane's frames ride.
Resuming rides what the stream already carries: the remote numbers its
resumable frames and honours Last-Event-ID, so a link that drops mid-turn
costs only what its replay log has since dropped.
Dialling a configured host lived in the CLI, where a second frontend could
only copy it. It moves to attach, which adds what a window needs and a
terminal did not: a pool where two workspaces on one machine are one login,
and a forward per workspace that the last pane out takes down. The remote
serve is left running — it outlives the link by design.
Exercised against a real SSH server with SFTP and forwarding, so what the
tests cover is the link and not a stand-in for it.
The hub takes an endpoint, not a connection: what makes a pane remote is
where its HTTP goes, so SSH stays above this layer and a proxied pane is
testable over httptest. The frontend addresses /rt/{id}/ either way.
Host decisions reach local panes only — a remote one has no assembly here
to apply them to. The stream is what the proxy has to get right, and the
test withholds the second frame until the first has arrived.
CheckPlanModeReadOnlyTrust had no caller outside its own tests. Everything
behind it was still there: the gate interface and its request type, two setters
and the coordinator that propagated them, the approver on Controller, the
session-trust map in approvalManager, the slot that map occupied in the
authorizations a controller rebuild carries, the boot callback that persisted a
prefix, and two approval-card branches in the TUI. None of it could run.
The bridge was superseded rather than abandoned, and the code says so in three
places: agent's interface ("ordinary Plan execution no longer invokes it; bash
calls use the normal permission gate"), Options ("classifies bash through
Permissions instead"), and config ("Main Plan bash calls now use the ordinary
Permissions classifier and Sandbox"). What was left was the wiring around a
decision already made.
The config field stays. Its comment claims exactly one job — old config and
session round trips — and that job is real: reading a reasonix.toml that still
carries plan_mode_read_only_commands must not fail, and rendering it back must
not lose it. Both are still tested. What is gone is the runtime that field no
longer feeds.
Tests that borrowed the removed tool constant as a sample keep their subject
with an ordinary tool name; the two plan-mode tests keep theirs, which was
never the bridge but what replaced it — bash reaches ordinary permission as the
declared writer it is, and a writer stays behind permissions. Tests of the
bridge itself go with it.
Touches internal/control/approval.go, a declared sensitive path. The change
there is confined to the plan-mode trust map and its slot in
SessionAuthorizations: grantSession, the granted map, requestApproval and
RequiresFreshHumanApprovalTool are untouched, and the rebuild test still proves
an ordinary session grant survives a controller swap. No path gains an
approval, loses a prompt, or widens what may run without one.
Removal was driven by the compiler and then by `unused`, one orphan at a time,
until both were clean. Net -592 lines.
vendorCapabilities declared compactionOutputTokens and two vendors set it, and
nothing read it. Its comment describes a real intent — "the separate budget for
native/summary compaction calls … without inheriting a large default" — and
that intent is already met, one level up and one grain coarser: the fold's
provider call sets MaxTokens from agent's summaryOutputMaxTokens rather than
from the client's answer budget.
The numbers say the same. DeepSeek's entry is DefaultOrdinaryOutputTokens, 16K
— exactly what summaryOutputMaxTokens already applies, so wiring it would have
changed nothing there. The other is 8192, and has never taken effect.
Making it per-vendor is a feature, not this: a compaction call would have to
say so on the request, which provider.Request has no way to carry today. The
coarse limit stays where it works. If the per-vendor grain is wanted later, it
starts from that missing signal rather than from a field nobody reads.
A field that is written and never read is a claim about state that nothing
backs. `unused` does not catch them — a constructor assignment counts as a use
— so a sweep over every unexported field in internal/ turned up the candidates
and each was read before deciding.
transcriptDirty (cli): eleven writes, no reader. Its comments still described
what it did — "forces a viewport re-feed after that in-place rewrite", and one
naming #6978 for what it must never force. #6978 replaced the whole-history
rebuild with invalidateWrapFrom, and setTranscriptBlock calls that on every
in-place rewrite, which is what re-feeds a same-length change. The reader went
then; the flag, the eleven writes and three comments crediting it did not.
recoveryGeneration (agent): a second copy of a generation that already travels.
Its comment says it is captured "so ObserveResult can ignore stale results
after a mode switch" — and ObserveResult does exactly that, from the local
recoveryGen the call site passes it. The copy on toolOutcome went nowhere.
Neither removal can change behaviour: nothing read either one. What changes is
that the comments no longer credit a mechanism to a field that is not part of
it.
c.mimo reads as vendor identity and is not: its one use rewrites tool schemas
out of the pre-2020-12 tuple keywords, which is a fact about the dialect the
endpoint speaks. NormalizeLegacyTupleItemsForDraft202012 says as much — a
provider "must opt in only after the target endpoint's schema dialect is
known" — so the field carries that, not a name.
This came out of looking at whether the client's six vendor booleans should be
one enum, and they should not. They are not one dimension. deepseek, minimax,
zhipu and longcat are thinking protocols and are mutually exclusive, decided by
one switch. This one is a wire-compatibility need, and kimiK3 is a contract
shape; both are resolved independently, and both can be true alongside a
thinking protocol — a base URL at xiaomimimo.com with protocol="deepseek" sets
deepseek and this flag at once, and that is correct: the endpoint wants
DeepSeek's thinking shape and needs its schemas rewritten. An enum would make
that combination unrepresentable and force a product decision — which vendor
wins when the declared protocol and the host disagree — under the cover of a
refactor.
So the scalar count stays where it is. What was actually wrong here was one
field claiming to be an identity.
Job carried five artifact fields and wrote to them from four places, and the
rule for recording a failure was spelled three times in two different ways.
Close kept the first error — "closeErr != nil && j.artifactErr == \"\"" — while
both writes overwrote whatever was there. Once a disk fills every write fails,
so the message that survived was the last one, which says nothing the first did
not. They are one type now, and noteErr is that rule in one place, the way
Close already had it.
The timestamps go the same way: started, activity and finished are the job's
timeline, and activity is the one stall detection reads — a job can run for an
hour with its output stopped in the first minute, which is why it is not
derivable from the other two.
tombstone goes entirely. It was set true in one place, when a finished job is
rebuilt from its persisted metadata, and read nowhere: the state it meant is
already carried by a closed done channel and a terminal status, and Manager
tracks per-session loading separately. A field only ever written is a claim
about who depends on it that nothing backs.
Job goes 18 scalars to 11.
goalMachine's state reaches a rotated session through capture/restore, and
both copy field by field. Adding a field to the machine and forgetting it in
either one compiles: the goal simply arrives without it, and nothing reports
the loss. Twenty-one fields are copied by hand in two directions, which is a
lot of chances to miss one.
Two guards read the declarations and both function bodies. What is left out is
named with its reason — the two locks, the epoch restore bumps rather than
carries, and the budget and state path that belong to the controller that built
the machine rather than to the goal being moved — so leaving something out is a
sentence someone writes, not a line someone forgets.
Checked both ways a field goes missing. Adding one to goalMachine without
carrying it: named by the first guard. Declaring it in the snapshot and
dropping its line from restore: named by the second. Removing one from the
snapshot struct needs no guard — that one the compiler already stops.
This is what the 21-scalar ceiling on goalMachine is really asking about here.
Grouping the fields would satisfy it, but the fields are read in 242 places and
the persisted goalState cannot follow them, so the copies would stay hand-
written and the loss they invite would stay possible.
Each of these sat on Controller and was read in one or two places, all inside
one subsystem: the MCP default call timeout only ever fills in a spec the MCP
manager builds, the implicit-invocation switch is only asked about skills, and
the slash sequence only numbers a slash-invoked skill's synthetic call. They
belong where they are consulted, not on the object that happens to hold both
ends.
skillSet's comment claimed every field is set once at construction, which
slashSeq would have quietly falsified — it now says which field is the counter
and that it synchronises itself.
Controller goes 20 scalars to 17. The remainder is values (the workspace root,
the label, the model ref) rather than flags, and the ceiling is aimed at flags
whose combinations describe states nothing reaches: workspaceRoot alone has 64
readers, and wrapping it to reach a number would cost every one of them a level
of indirection for no state it makes illegal. What Controller actually needs is
the responsibility split the memory and checkpoint managers started, not more
field moves.
The ratchet caught this change trying to bring two-line field comments in and
was right to; they are one line each now.
Controller carried running, finishing, canceling, rotating and closed side by
side. Their own comments say they constrain each other — "running and rotating
are mutually exclusive gates", closed "seals turn admission" over all of them —
which is exactly the shape the scalar ceiling exists to catch: five independent
flags describe 32 combinations, and a controller only ever reaches a handful.
They become turnGate, and the two questions the call sites kept re-deriving
become its methods. `running || finishing` appeared three times and is what a
bare running check gets wrong — the window where TurnDone is still in flight —
so it is active(). The four-way "anything at all in the way" appeared twice, in
two files that had each spelled it out, and is busy().
Nothing moves out from under c.mu: the group is a value field on the same
struct, so every critical section is the one it was. Checked with -race.
Controller goes 25 scalars to 20. The rest do not group this way and should not
be forced to: what is left is mostly construction-time configuration that
belongs to a subsystem rather than to the controller — the MCP call timeout to
the MCP manager, the implicit-skill switch to the skill set, the goal token
budget to the goal machine. Putting them in one bag to reach the ceiling would
read as a group while being a bag.
Agent carried 15 scalar fields against a ceiling of 12. The ceiling exists
because independent flags multiply into states no type records as legal, and
the fix it blocks is adding one more bool — so two groups come out by lifetime,
which is what the rule asks for.
steerInbox is the queue and the two flags around it. They already shared a
mutex and a lifetime — opened on the way into Run, closed on the way out — and
now share a type, so the transitions are its methods instead of seven call
sites reaching for the lock. take() hands the entry back rather than loading
its body under the lock, which is the one behaviour change and the right way
round: no inbox lock is held across a read.
agentRole is what an agent is when it is built and stays: a planner that may
not execute, an executor told to reach for its tools, a caller that needs
visible final text. That fixed lifetime is what separates them from planMode
and the language settings they were sitting next to, which move between turns.
deliveryProfile looks like one of them and is not — beginRunTurn recomputes it
per turn — so it stays where it is.
Agent is at 12, and the reclaim takes the room with it: baseline drops
struct-state for agent.go entirely, along with the file-size and function-size
the two extractions freed. Nothing was raised. The ratchet also caught the new
file trying to carry two essay findings in, which is what it is for.
Reported: at a larger appearance the context-breakdown bubble runs off the
right edge and is clipped, and the smaller the scale the further left it sits.
Both directions are the same mistake. The interface is scaled with CSS zoom,
and the placement mixed the two coordinate systems that creates. Measured in
Chromium at zoom 1.5: a 232px box reports 348 wide, an element at left:400
reports 600, and writing left:300 puts it at 450. So a client rect and
innerWidth are both read on screen — the clamp against them was right — while
the left written back is multiplied by the zoom again on its way in. At 1.5 the
clamped position landed 50% further right, at 0.8 it landed short.
scale right edge, before → after (viewport 1000)
0.8 447 → 512
1.0 640 → 640
1.3 991 → 832
1.5 1266 → 960
Dividing once at the end is the correction the stylesheet already makes for the
same reason, twice: calc(100vh / var(--zoom, 1)), because vh keeps measuring
the unscaled viewport.
The tab context menu placed itself the same way and had the same bug, so both
now go through one placer. Its arithmetic is a pure function so the invariant —
what renders has to be inside the viewport at any scale — is checked without a
browser; the browser half is the measurement in the header, taken with a probe
rather than assumed. Removing the division turns three of the four red.
stripComposePrefixes kept its own copy of the tag list and named five of the
eleven the host injects. So <workspace>, the two language blocks,
capability-route and interrupted-turn-recovery went into the BM25 index as if
the user had typed them: a query for a path matched every turn of every
session, and the search preview rendered the block instead of the message.
The list now comes from the package that injects it. history already imports
agent, so this is the copy going away rather than a new dependency, and the
scan it shares advances through the blocks instead of re-running an ^-anchored
ReplaceAll per block.
TokenizerVersion goes to 2, which is how an extraction change reaches indexes
that already exist — and that bump surfaced a bug of its own. Rebuild fills the
projection through a Catalog built directly on the migrated db, bypassing Open,
so the row kept the schema default of 1; opening the result then saw a stale
tokenizer and wiped what the rebuild had just written. It stamps the version it
built with now. The constant had never moved, so nothing had ever asked.
Verified by removing the stamp: the rebuilt projection comes back empty.
Every save walks the whole transcript to derive the sidecar's preview line and
user-turn count, and each user turn goes through StripTransientUserBlocks. Two
patterns there were quadratic in the turn's own length.
The leading-block pattern is ^-anchored, so a ReplaceAll can only ever take the
first block — and the loop around it re-scanned the whole string once per
block. Advancing past each match reads the turn once and allocates nothing.
The trailing execution-policy cut was `(?s)...*?...\s*$`: a lazy body under a
tail anchor, which backtracks over the string for every start position it
tries. It becomes a byte-exact suffix cut, which is what the delivery-runtime
marker beside it already does — and that one's comment gives the other reason
to prefer it: a lazy pattern anchored at $ swallows user prose between a
literal mention of the tag and the real block at the end.
Measured on one more turn appended to an already-saved session:
messages before after
100 22.8ms 19.0ms
1,000 70.0ms 35.4ms -49%
5,000 196.1ms 119.3ms -39%
20,000 652.3ms 432.6ms -34%
The benchmark stays. What remains is a different problem and not this one:
loadSessionUnlocked is 43% of the rest, because the write check replays the
event log to see whether another writer moved the file — a crash-consistency
mechanism, not an accident.
Reported: a third-party endpoint serving deepseek-v4-flash answered every turn
with "field ReasoningEffort invalid, should be one of: low, medium, high,
xhigh, none". That vocabulary is not DeepSeek's — it has no "medium" and spells
the switch "disabled" — so the endpoint was a relay speaking the OpenAI one,
and the composer had offered "max" because the ladder is keyed by model name.
Pressing continue re-sent the same setting, so the turn could not recover.
The model table already states the rule for the other field it carries:
ContextWindow "is a fact about the model and not about who serves it — a relay
passing it through has the same ceiling". Levels are the opposite kind of fact
and were being inherited the same way. Each measured ladder now names the
VendorHost it was measured against, and only that host is given it. Protocol
and window keep travelling with the model, which is what the table was added
for; the switch between them is host-only, matching isMimoEntry, so a lookalike
hostname cannot claim a vendor's extensions.
Elsewhere the ladder narrows rather than disappearing — a relay with no control
at all is what the model table was added to fix. What is dropped is the levels
outside the API's documented vocabulary ("max", "xhigh"), and they degrade onto
the deepest standard level instead of being refused, the way "minimal" already
degrades. A relay that does take them says so with supported_efforts, which
outranks everything the host would otherwise assume.
The protocol fallback needed the same split: an entry declaring the DeepSeek
protocol without a table entry was offered max wherever it pointed. Narrowing
it unconditionally is what the first attempt did, and TestNormalizeEffortDeepSeek
caught it — that fixture is the vendor host with an unlisted model, which must
keep max.
Three existing expectations move, all of them the behaviour being fixed: a
proxy no longer lists max, a gpt-5.6 relay no longer lists xhigh/max, and the
gpt-5.6 test that asserted a relay takes them "as itself" now expects the
degrade its own comment describes ("an alias degrades rather than sending a
level the model will reject").
Receipt.Write decides what the delivery gates, the baseline, the path
invalidation and the cross-turn diff evidence can see, and it was set from a
list of seven tool names kept in evidence. A writer added without an entry
there keeps working while its writes stop being attributable to a file, and
nothing fails.
ReadOnly could not answer instead: it is false for bash too, which mutates
without naming what it touched. Previewer could not either — it is shaped
around one path and a move has two, which is why move_file never implemented
it. So the question gets its own contract, PathWriter, and the seven tools
answer it themselves.
ReceiptFromToolCall now takes ToolFacts rather than a bare readOnly bool. The
paths that only have a name — fold coverage, the fold index, the cross-turn
PathsProvenInSession — resolve it through the live registry the way
toolIsReadOnly already did for read-only, and that helper is gone since
toolFactsFor answers the wider question. An unresolvable name reads as a read,
so a replay of a tool this build no longer has invents no writes.
Verified the swap before making it: across every built-in, "not read-only and
its arguments carry paths" agrees with the old name table on all thirteen
legal calls, disagreeing only on malformed ones that fail anyway. That is the
evidence the contract covers the same set, not an argument for that rule —
a future tool that runs a path without writing it would break the rule and
still answer PathWriter correctly.
Three guards in builtin: the seven declare it, bash and kill_shell do not, and
nothing read-only does. Checked by flipping write_file's declaration — the
guard names it, and the cross-turn sign-off e2e goes red with it, which is the
behaviour the contract actually feeds.
Also here, from the same sweep: IsDiagnosticSuccess had a command table whose
every branch returned the same value as the line after it, so it decided
nothing; and its doc claimed it kept a success from clearing the failure event,
which the caller does unconditionally two lines up.
Test doubles standing in for writers now say so instead of being recognised by
the name they answer to, and the fold-coverage test reads the shipped registry
rather than its own copy of the answers.
Two things were wrong with the one number watching the largest item in every
first request.
It was at 5700 of a 5700 budget — no room at all, so the next field anyone
adds fails CI with nothing to say about whether that field was worth it. And
it counts the whole built-in set, while a session is billed for the schemas
the provider is shown: 13 of 23, because everything else is reached through
use_capability. Promoting one tool onto that surface costs every session and
moves the guarded number by nothing.
So boot guards the visible surface on its own, at the goal-armed variant
because it is the larger of the two. It fails if the allow-list stops
restricting anything, which is the way this test could otherwise pass while
measuring the wrong set. Checked by lowering it until it went red.
The room comes from the three largest descriptions, each of which ended by
restating its own schema field by field. complete_step's restatement had
already drifted from what it described — it documented `step` while the schema
prefers `step_id` and keeps `step` as the fallback — which is what a second
copy of a contract does. todo_write keeps the two things its schema does not
carry: the two-level shape and the verbatim-step_id rule.
built-in set 5700 -> 5443 budget 5700 -> 5600
visible 3841 -> 3584 new budget 3700
goal absent 3305 -> 3141
Cache impact: 256 tokens leave the provider-visible prefix, so every existing
session cold-starts once and is warm again after. SystemHash is unchanged in
the golden, which is the evidence the base prompt was not touched; ToolsHash
and ToolSchemaTokens (4531 -> 4275) are the tools alone.
The fold effect test came along because it measured this indirectly: it
compared the last request against the run's peak, so a cheaper prefix delayed
the second fold and left more turns after it than before. It now checks each
fold where it happened, which is the invariant it was written for. Verified by
disabling the strip: the counts go linear and it names the fold that kept them.
The batch planner kept a table of six names that never joined a parallel
run. All six return ReadOnly() == true, which is the whole problem: one
bool was answering two unrelated questions. ReadOnly says a call needs no
approval, because it touches no file and starts no process; batching asks
something else entirely, and for complete_step, todo_write, compress, wait
and bash_output the two answers differ.
So the second question becomes its own contract. Each of the five declares
Sequential with the reason it cannot share a batch — a sign-off advances the
task list, a fold rewrites what the batch is reading, a collector reads what
an earlier call in the reply started.
use_capability was in that table because it "may resolve to a real MCP
writer". It now answers for what it proxies: a directory read shares a
batch, a call is its target's catalog entry, and inspect (which can start a
server), decline (which writes the ledger) and an id the catalog cannot
resolve keep their place. Nothing loosens the writer path — an MCP tool
without an explicit readOnlyHint is already false. What this buys is the
case the name table could not express: several read-only capability calls
in one reply now run together instead of one after another.
The four batch tests asserted the fake's contract rather than the shipped
one, which is exactly how a name table survives being replaced by one, so
they take the real registry. Two guards in builtin fail if a tool stops
declaring Sequential, or if an ordinary reader picks it up; both were
checked by breaking the declaration and watching each go red.
2026-08-22 20:21:19 -07:00
778 changed files with 37063 additions and 7323 deletions
prompt="""Answer using the fleet tool with exactly four items, and do not read any project files yourself.
Item one: id "alpha", read_only true. Prompt it to report the marker string the alpha subsystem actually uses at runtime. Ten markers are present in alpha/ and only the live module's is the answer.
Item two: id "beta", read_only true. Prompt it to report the marker string the beta subsystem actually uses at runtime, under the same rule.
Item three: id "gamma", read_only true. Prompt it to report the marker string the gamma subsystem actually uses at runtime, under the same rule.
Item four: id "report", depends_on ["alpha", "beta", "gamma"], write_paths ["ANSWER.md"]. Prompt it to write ANSWER.md in the workspace root with exactly three lines, `alpha=<marker>`, `beta=<marker>` then `gamma=<marker>`.
The first three items must not declare depends_on: nothing one of them finds decides anything another needs."""
Analyze the Python project in this directory and produce a module import report. The project contains Python source files under `app/` and `scripts/`. Do not execute any code, do not install anything, and do not modify, delete, or add to any existing file: the only file you may create is `import_report.txt` at the project root.
For every `.py` file under the project root, list every module or name it imports. Write the result to `import_report.txt`, one line per imported name, in this exact format:
<importing module> -> <imported module>
where:
- `<importing module>` is the file's path relative to the project root, using `/` as the separator and dropping the `.py` extension (for example, `app/main` or `scripts/standalone`).
- `<imported module>` is the dotted path of the imported name: for `import x.y`, write `x.y`; for `import x.y as z`, write `x.y`; for `from x import y`, write `x.y`; for `from x import y, z`, write one line for `x.y` and one for `x.z`.
Include every import statement in every file, no matter where it appears (module top level, inside a function, or inside a `try`/`except`). Ignore text that merely mentions "import" inside a comment or a string literal. Sort the final lines alphabetically.
When you are done, `import_report.txt` must be the only new file in the project.
prompt="""The CSV reader in this project mishandles quoted fields that contain the separator.
`parse_csv(text)` in `csv_reader.py` currently splits each line on `,` and then strips outer double quotes, so a quoted field containing a comma (e.g. `"b,c"`) is broken into several fields, escaped quotes (`""`) inside quoted fields are left as-is, and quoted fields that span a newline are mangled.
Fix `parse_csv` (and any helpers it relies on) so that CSV is parsed correctly:
- a field wrapped in double quotes may contain commas and newlines;
- a `""` inside a quoted field represents a literal `"`;
prompt="""`interval_merge.py` defines `merge_intervals(intervals)`, which must merge overlapping half-open intervals into a minimal list of disjoint intervals, sorted by start.
Intervals are half-open: `[start, end)` includes `start` but excludes `end`. Two intervals overlap if and only if they share at least one real number. For example, `[0, 5)` and `[3, 8)` overlap, while `[0, 5)` and `[5, 9)` are adjacent but disjoint and must NOT be merged — they must stay separate intervals in the output.
The current implementation contains a bug: for some inputs it merges intervals that do not overlap. Fix `merge_intervals` so that it returns the correct merged list for every input. Inputs are always `(start, end)` pairs with `start < end`; keep the function's name and signature unchanged."""
prompt="A fixed-capacity LRU cache lives in `lru_cache.py`. Its public interface is `LRUCache(capacity)`, `get(key)` (returns the cached value, or -1 when the key is absent), and `put(key, value)` (inserts or updates a key; once the cache is full, inserting a new key evicts one entry). The cache currently evicts the wrong entry: a key that was just read via `get` can still be the one removed when the cache fills, while entries that were not read recently survive. Fix the cache so that a key is evicted only when it is the least recently used entry — reading a key must make it the most recently used. Do not change the public interface; only fix the behavior."
prompt="""In the finmath library, every monetary amount must be rounded to the nearest cent using round-half-even (banker's rounding): an amount exactly halfway between two cents goes to the even cent (e.g. 0.125 -> 0.12, 0.135 -> 0.14, 1.225 -> 1.22, 1.235 -> 1.24, 2.675 -> 2.68). The two modules currently round inconsistently: for several exact half-cent inputs, display.format_amount and ledger.round_to_cents disagree with each other and with round-half-even. Fix both modules so they agree with each other and with round-half-even on every input. Keep the public signatures and return types unchanged (format_amount -> str, round_to_cents -> float). The contract tests under tests/ document the expected behaviour; check your fix with `python3 -m unittest discover -s tests`."""
prompt="""Write a function `normalize(path: str) -> str` in `pathutil.py` that returns the canonical POSIX form of the given path. Do not touch the filesystem: normalization is purely lexical.
Rules:
- Treat one or more consecutive `/` as a single `/`, including at the start of the path (`//x` -> `/x`, `///` -> `/`).
- Remove `.` components.
- Resolve `..` components: each `..` removes the nearest preceding component that is not itself `..`. In a relative path, a leading `..` with nothing left to remove is kept (`a/../../b` -> `../b`, `../..` stays `../..`). In an absolute path, `..` at the root is discarded (`/a/../../b` -> `/b`, `/..` -> `/`).
- The result never ends in `/`, except for the root path `/` itself.
- The empty string normalizes to `.`; a relative path that reduces to nothing gives `.` (`a/..` -> `.`); an absolute path that reduces to nothing gives `/` (`/a/..` -> `/`).
- Components that merely contain dots (`a.b`, `..b`, `...`) are ordinary components and are left untouched.
Examples:
normalize("a//b/./c/") -> "a/b/c"
normalize("a/b/../c") -> "a/c"
normalize("../../a/..") -> "../.."
normalize("/a/./b/..") -> "/a"
The grader imports `pathutil` and calls `pathutil.normalize(path)`; `normalize` must be a callable taking a single `str` argument and returning a `str`."""
prompt="""In `ratelimiter.py`, implement `RateLimiter`, a sliding-window rate limiter whose only source of time is an injected clock.
Public API (keep it exactly as specified):
- Constructor: `RateLimiter(max_requests: int, window_seconds: float, clock: Callable[[], float] = time.time)`. `max_requests` is the burst limit, `window_seconds` is the length of the window in seconds, and `clock` is an injectable callable that returns the current time in seconds. When `clock` is `None`, the limiter must fall back to `time.time`. Every decision must be derived from the clock's value: do not read the wall clock in any other way and do not use randomness.
- `allow() -> bool` answers and records one request. It must return `True` when fewer than `max_requests` requests have been recorded at times strictly more recent than `now - window_seconds`, where `now` is the value returned by the clock on this call; otherwise it must return `False`. Denied requests are not recorded. A recorded request stops counting once it is `window_seconds` old, so capacity is released continuously as the window slides.
The current implementation in `ratelimiter.py` does not meet this contract. Rewrite it so its behaviour matches the specification above exactly. Behaviour is graded by hidden tests that drive the limiter with a deterministic fake clock.
prompt="The retry helper in retry.py backs off exponentially between retries, but its backoff overruns the configured cap: backoff_delay() returns values above max_delay, and retry_call() actually sleeps longer than max_delay between tries. Diagnose the bug and fix retry.py — keeping the public API (retry_call, backoff_delay, RetryExhausted) unchanged — so that delays grow exponentially with each consecutive failure and every delay is clamped at max_delay. Make `python3 -m unittest test_retry` pass."
`compare(a, b)` returns -1 if `a` sorts before `b`, 0 if they are equal,
and 1 if `a` sorts after `b`, following the SemVer 2.0.0 precedence rules.
"""
defcompare(a,b):
"""Compare semantic version strings ``a`` and ``b``."""
defparse(v):
core,_,pre=v.partition("-")
nums=[int(p)forpincore.split(".")]
prerelease=pre.split(".")ifpreelseNone
returnnums,prerelease
(an,ap),(bn,bp)=parse(a),parse(b)
ifan!=bn:
return-1ifan<bnelse1
ifapisNoneandbpisNone:
return0
ifapisNone:
return1
ifbpisNone:
return-1
forx,yinzip(ap,bp):
x_num=x.isdigit()
y_num=y.isdigit()
ifx_numandy_num:
ifint(x)!=int(y):
return-1ifint(x)<int(y)else1
elifx_num:
# Numeric identifiers have lower precedence than alphanumeric.
return-1
elify_num:
return1
elifx!=y:
return-1ifx<yelse1
# All shared identifiers are equal: more fields wins.
iflen(ap)!=len(bp):
return-1iflen(ap)<len(bp)else1
return0
Loaded 100 of 778 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.