#13812 made the dispatch-time profile the concrete runtime. The effort-switch audit
test (merged beside it) holds that profile to what the call selected (empty model and
effort when it inherits), so studio went red. The selection is audited as the model
asked it; what the child ran on is on its status events, which the window already
reads, so the dispatch profile goes back to the selection.
The panel row, the full-record header and the nested card of the spawning call show
provider/model · effort, with the full reference as the title. The card takes it
from the child's status events, so a failed or interrupted run keeps it; a batch's
items each carry their own.
The dispatch profile now names the concrete runtime a call resolves to (an alias
expanded, the parent's model when the call inherits), and each child's status events,
from queued to the terminal one, carry what its run actually started with, per item
for a batch. A failed or interrupted child therefore still says what it ran on.
Bumps the go group in /desktop with 1 update: [golang.org/x/mod](https://github.com/golang/mod).
Updates `golang.org/x/mod` from 0.41.0 to 0.42.0
- [Commits](https://github.com/golang/mod/compare/v0.41.0...v0.42.0)
---
updated-dependencies:
- dependency-name: golang.org/x/mod
dependency-version: 0.42.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: go
...
Signed-off-by: dependabot[bot] <[email protected]>
Two controls the audit's contract implies but nothing pinned: a staged copy whose
retired twin came from a different publish is left untouched (no pair, no writes),
and a fact created at the old name after the kill is never overwritten — the retry
refuses and keeps both artifacts for a later attempt. The refusal only named the
conflict once the exist check unwrapped the error, so it now uses
errors.Is(err, fs.ErrExist).
A fold that loses the transcript race records FailContextChanged, but no later
turn could release it: retriesOnGrowth did not name the code, the receipt holds
until the generation advances, and the generation only advances on a commit the
held receipt prevents. One racing append then latched automatic compaction off
for the rest of the session, across restarts, and the next overflow failed the
turn instead of folding.
Name the code in retriesOnGrowth so the append that made the fold stale is the
growth that releases the receipt. The receipt already carries InputHash and
InputTokens, and every other code keeps its hold. A plan/provider refusal still
holds, and the retry buys exactly one fold: without further growth the next
prepare neither folds again nor re-records the block.
The recovery may either finish the rename forward or roll it back. Only rollback
keeps an ExpectedRevision from before the rename valid, so the choice is a
contract, not an implementation detail; forcing the forward path makes this test
fail with name=beta revision=2.
publishRename retires the old fact file, flushes the index, then publishes
the staged new file. A kill in that window leaves the fact's only copies as
hidden .retired-* and .staged-* files: Read/List/Revisions report nothing,
the save retry by id reports "memory id not found", and a retry by name
mints a second identity for the same name.
A save by id that finds no active file now looks for the hidden pair a
single publishRename call writes, matched by the nanosecond stamp in the
two file names, with the staged copy's frontmatter carrying that id. The
rule is disk-driven and rollback-first: readable retired bytes are restored
under the old name (same bytes and mode) and reindexed, so the caller's
expected revision still names the fact and the retried save redoes the
rename itself. Only when the retired copy cannot be parsed does recovery
publish the staged copy forward under its new name. With no matching pair
nothing is written and the id is still reported not found, so a retired
copy left by a committed rename is not revived.
Covered by the audit's kill probe, an uninterrupted-rename control, a name
retry after recovery, a failed (not killed) publish, an unpaired-artifact
guard, and a cross-scope move whose pair spans two directories.
Review follow-up after rebasing onto studio: messages_en/zh/zh_tw.go sit at the
800-line ceiling, so the two new strings move into the embedded QueueMessages and
NotifyMessages catalogues, which have their own files. No call site changes.
session.read and session.submit are separate capabilities, so a mod that only
reads usage cannot put words in front of the model. One prompt a mod queues is
bounded by [mods] submit_bytes (8 KiB, 0 for none) and refused with a typed
identity that does not spend the per-turn count. The docs say the turn a queued
prompt starts is held to this session's own approvals and sandbox.
Problem: Studio browser guards failed on 17f816d11 when the ordering guard
counted SSH rows as conversations in its local project. The remaining quick
action fixture also expected two trailing Archive/Delete buttons.
Root cause: Browser acceptance scripts still encoded the former separate
machine lists, trailing Delete action and individual folding chevrons. The
English wording scan also lacked a newly visible demo conversation title.
Fix: Scope ordering to the named local project and independently assert SSH
rows and keyboard focus are preserved. Exercise default-off and explicit
quick-action choices for local and remote rows, including hit targets,
leading Pin/trailing Archive geometry and menu access. Load the real row
stylesheet in the compact fixture and match its existing 1px spacing while
retaining 34px rows, ellipsis and contained controls. Update manual tree
checks to use project-row folding and five-item previews.
Recognize the existing demo title as user content in the wording fixture list.
Verification: Reproduced the ordering, quick-action and fixture failures
before the repair. Ran all 55 CI browser-guard commands against the built
perf harness and reran repaired guards, the affected manual tree scripts,
gofmt, root vet, make lint,
make check and the required builtin/boot suites. This changes acceptance
scripts only; production UI, workflows and budgets are unchanged.
The reviewer's blocking item: rebindInbox (called from New) started a goroutine
that read c.sink while New was writing it. Capture the sink before starting the
goroutine, for the new unrecovered notice and for the pre-existing recovered one
(it had the same shape and the same race).
Also give the refusal an identity: ErrUnrecovered, wrapped by commitManifestLocked,
with a case in the serve switch so the frontend gets inbox.unrecovered instead of an
internal fault. repolint's refusal-path rule requires that case.
The store already told a caller that the queue's bodies could not be read
(InboxSnapshot.Unrecovered), but nothing user-facing consumed it: the TUI's
/queue line and the notice stream both dropped the field, so an unreadable
queue still read as "inbox empty" and the user could not tell it from a
genuinely empty one.
The TUI mirror and renderQueueList now carry and say the unknown state, and
the controller emits a distinct inbox_unrecovered warn notice (with an
InboxUnrecovered payload naming the queue directory) beside the existing
inbox_recovered one, so its sentence is not confused with "recovered N".
Cause: the frontend contract field had no consumer on the TUI or notice
surfaces.
Blast radius: TUI /queue line, TUI and desktop notice wording, and the two
controller inbox open paths; additive field, code and payload, no on-disk
schema change, recovered behaviour unchanged.
Neighbouring behaviours tested: empty-but-readable queue, recovered salvage
count, TUI mirror JSON decoding of the real GET /inbox body, and both the
serve and control boundaries.
Why this layer: the store owns "unknown vs empty"; the surfaces that render
a queue are where a user can act on it.
Provider-visible prefix: none
salvageOrphanBlobsLocked returned nil for both "no blobs" and "blobs
unreadable", so a corrupt manifest plus an unreadable blobs directory
committed an empty paused manifest (recovered=true, recoveredN=0,
items=null) over the quarantined one. The queued bodies stayed on disk but
were unreachable, the RecoveredN>0 notice never fired, and the queue looked
empty.
The salvage now reports the read failure, and that failure is not turned
into a manifest: the corrupt bytes stay in place as evidence, the store
comes up paused with unrecovered=true on the snapshot, and no commit can
overwrite the evidence while the bodies are unreadable. An absent or empty
blobs directory is still an empty queue, not a failure.
Cause: (items, err) collapsed to (nil, nil) for directory-level and
per-entry read errors.
Blast radius: sessioninbox Open/Snapshot plus the commit path; new
additive InboxSnapshot.Unrecovered field, no on-disk schema change.
Neighbouring behaviours tested: readable-dir salvage, absent/empty blobs
dir, healthy manifest untouched, symlink blob refusal.
Why this layer: the store owns the manifest/blob transaction, so the
"unknown vs empty" distinction belongs where the read fails.
Found while demonstrating the install through Discover: until the switch and
both components read Running there is no reasonix tool, and the model falls
back to running reasonix -p in its own shell, which only looks like a
delegation.
An @token in a peer message or a background-job notice was frozen into the
prompt as a file reference, so a sender could make the receiver read a file
into its prompt, and an unreadable one paused the queue behind it. Items the
host authored are data and are no longer scanned.
The host keeps the text of a task's external results and counts, per turn, how
many were recorded and how many later non-read-only calls carried a verbatim
string from one. The numbers ride the completion-report audit; nothing is
blocked or shown to the model.
Problem: Review requested WSL boundary evidence and removal of the unrelated
design-qa.md diff after studio introduced non-SSH machines.
Root cause: The new WSL transport supersedes a blanket rejection at shared
remote endpoints. SSH construction must still reject WSL entries, while
generic probe, browse and open must select their configured transport.
The earlier QA cleanup also deleted a document already owned by studio.
Fix: Integrate studio through 0363720bd and cover the real Hub/remotehost/pool
route with disposable WSL and SSH factories. Assert WSL failures survive the
HTTP boundary and never construct an SSH client. Cover Windows, distribution,
empty and fallback default paths. Restore design-qa.md byte-for-byte from
studio so it no longer appears in the PR diff. Retain the upstream SSH form's
refusal to overwrite non-SSH entries.
Verification: Frontend 5529 tests and production build; full serve,
remotehost, remote, attach, wslconn, config, builtin and boot suites; gofmt,
root vet, make lint and make check. No live Windows/WSL qualification claimed.
Problem: PR #13638 became conflicting after studio introduced moving a
conversation to another project.
Root cause: HubPort acquired moveSessionTo beside the archive declaration
that this branch extends with an optional host. Automatic merging also
omitted the errors import needed by the upstream WSL edit guard.
Fix: Keep both capabilities and restore the required import. Retain the
upstream local-only move workflow and the host-qualified pin/archive routes.
Remove an inaccurate rename comment so the combined component stays within
its existing size ceiling without widening the baseline.
Verification: Frontend suite (5529 tests), production build, full serve,
sessionstore, config, builtin and boot tests; gofmt, root vet, make lint and
make check. Transport coverage exercises local project moves beside remote
flags; an interaction regression covers the shared keyboard menu and dialog.
An isolated development browser confirms menu access, project selection and
cancel with hover controls off. Hosted CI remains a separate observation.
The index pins the commit that last changed the package; scripts/dsh-market-index.mjs
pins it and validates it with DSH's own validator. docs/DSH.md explains Discover
and the Studio release runbook gains a precondition that keeps the pin current.
A configuration-only package (no code) in integrations/dsh-plugin, installed
from this repository's subdirectory with pnpm's path: selector, plus the
integration guide docs/DSH.md.
How to wire dsh's subagent-acp provider to reasonix acp, what each permission
policy does to Reasonix, what the parent sees, and the limits, verified against
dsh 0.2.1-rc.2.
session/new answered -32603 for a model with no API key or no configured
provider, although initialize advertises a setup auth method. The cause is now
a provider.ErrKeyMissing / ErrNoProvider identity and the frontend maps it to
the ACP schema's -32000.
Rule files under .reasonix/rules, .claude/rules and the user's rules folder are
read from disk each turn. Rules without paths: join the turn as a block owed
like the standing instructions; rules with paths: are owed once the session has
touched a matching file and retracted when they stop applying. Glob parsing and
matching stay with skills' paths: through one exported entry point. Neither layer
reaches the cache-stable prefix; capability diagnostics report what was skipped.
agent.default_mode (user file only, off by default) is applied by the assembly
for builds that open a fresh conversation: Studio panes without a saved
session, the TUI without a resume, and ACP session/new. A resume, a headless
run and sub-agents never take it. Settings gets the switch.
The unverified-change gap names the command the host declined, bounded
at 80 bytes by command[:80]. A check with non-ASCII paths, common in a
Chinese project, could be cut inside a character, and the delivery card
then showed a replacement glyph before the cut mark. Step back to the
start of the character instead, keeping the budget and the mark.
Subprocesses the host starts for the agent carry REASONIX_AGENT_CHILD=1, and the
person-only schedule commands refuse when it is set, so a pty opened by a tool
no longer passes the terminal check. add prints the whole plan and requires its
short id typed back; list shows how each schedule was confirmed. The Windows
preview test compares against the resolved workspace path.
The host also answers 'schedule exec' by running the CLI's entry, so the
supervisor can start it as the child. One kernel at a time ticks, under the
machine's ticker lock.
Scheduler.Tick reaps dead runs, claims at most one due slot inside its grace
and hands it to the supervisor; the schedule add, list, pause, resume, renew,
rm and tick commands drive it. Creating, resuming and deleting need a
terminal.
$/cancel_request cancels only the named request and answers -32800;
agent message chunks carry an opaque messageId; an MCP server's form or
page goes through elicitation/create when the client advertised the mode.
Problem: Studio's new SearXNG connection probe conflicts with the persona
types in three transport import lines, blocking this PR again.
Root cause: independent settings features append types to the same line.
Fix: normally merge Studio 6f68a14a9, preserving the search probe and persona
contracts. Keep persona-only imports separate so subsequent upstream type
additions do not repeatedly conflict. Remove one upstream trailing space.
Retain the upstream MCP scope upgrades, ACP session directories, Gemini
provider, session ordering and rail-search behavior without modifying their
implementations. Persona prompt, user-only scope and guarded writes remain
unchanged, as do the approved composition UI and default-off behavior.
Verification: three-way simulations of future settings imports merge cleanly
in all three files. Format, vet, lint, make check, required builtin/boot tests,
full root Go tests with atomic coverage, full CI race package group and Desktop
Go pass. Frontend typecheck/build and 662 files / 5425 tests pass. Chromium
checks cover persona navigation in both locales, light/dark, wide/390px;
rail search, session ordering, web access, settings rows and font scaling.
Local review and security review check the integration and fresh MCP consent;
they do not replace maintainer approval. No gate or budget was relaxed.
Problem: the sidebar PR conflicts with studio's new per-session move actions.
Root cause: both branches extend the row key handler and the mock tree's
session projection. Sorting shortcuts also bubbled into tree navigation,
moving focus to a different session after a reorder.
Fix: retain keyboard context menus and session moves together, compose mock
placement with pin/archive decoration, and consume sorting keys at their
owning row. Keep the existing remote host and body-size guards unchanged.
Verification: 5478 frontend tests across 669 files; production frontend
build; full serve, sessionstore, builtin and boot suites; gofmt, root vet,
make lint and make check. A failing-first keyboard regression and isolated
browser checks prove reordered rows retain focus and keyboard menus still
expose pin, archive and move actions.
A 403 insufficient_scope from a signed-in server now stops the model and
tells the user; granting more is a fresh approval asked every time, from
the server row or reasonix mcp auth --upgrade.
Problem: the latest Studio session-reference settings overlap the persona
port imports and development proxy, leaving this PR in conflict.
Root cause: both features add settings types at the same transport boundary.
Fix: normally merge Studio 8c311b02b, retaining both Persona and SessionRef
contracts, Mock state, SSE methods, routes and settings panels. The upstream
dispatch-consent, bounded-fold and subagent-view implementations stay intact.
Persona prompt assembly, user-only scope, guarded settings and save timing
remain unchanged. The preceding test-only commit synchronizes cancellation
observation without changing the mock provider or relaxing error/close checks.
Verification: formatting, vet, lint, make check, required builtin/boot tests,
full root Go tests with atomic coverage, the full CI race package group and
Desktop Go tests. Frontend typecheck/build and 660 files / 5411 tests pass.
Chromium checks cover persona/settings navigation in Chinese and English,
light/dark and wide/390px layouts; upstream session-reference interactions,
settings rows, font scaling and explorer counts are covered as well.
Problem: the hosted race group failed the mock provider cancellation test
when the next received chunk was an already-pending tool call.
Root cause: cancellation does not prioritize the Done case over a send that
is also ready in select. The test canceled the context and immediately read
again without establishing that the producer had observed cancellation.
Fix: acknowledge cancellation through the test context's Err method before
draining the stream. Retain the strict cancellation-error and subsequent
channel-close assertions, plus the unchanged pre-canceled request test.
The hang guard uses the existing test budget. No mock/product behavior,
runtime prompt, UI, thresholds or workflow approvals change.
Verification: reproduced one failure in 10000 race-instrumented iterations
with GOMAXPROCS=2 before the change; 10000 final iterations pass afterward.
A temporary explicit-marker mutation that ignores cancellation is rejected.
The complete CI race package group passes, alongside gofmt, vet, lint,
make check and builtin/boot tests. Prior frontend/coverage evidence applies
to the unchanged product tree.
The prompt result carried a top-level transcriptPath the ACP schema does not
allow, and status_update / mcpStatus_update went to every client, which
standard SDK clients answer with a method-not-found error each. The two
notifications now need clientCapabilities._meta["reasonix.io"].statusUpdates
and the path moves under _meta["reasonix.io"].
Problem: newer Studio settings UI and persona translations both appended to
the shared English catalog, leaving the PR conflicting. Its new setting-open
notices also lacked a destination for the guarded persona fields.
Root cause: translations for an independent settings domain shared an append
point, and the newly introduced frontend routing table did not know persona.
Fix: normally merge Studio 15de9ec33, preserve both sides' translations and
extract persona's unchanged entries into its own catalog module. Route the
three guarded persona setting ids to their editor and cover actual notice
clicks. Preserve the persona's user scope, supplemental prompt semantics,
save timing, settings navigation and existing authorization boundaries.
Verification: format, vet, lint, make check, complete root Go tests with
atomic coverage, Desktop Go, frontend typecheck/build and 657 files / 5400
tests. Verified all 25 extracted translations unchanged. Chromium navigation
passed in Chinese/English, light/dark and wide/390px layouts; settings rows
and font scaling pass. The later dispatch config/catalog combination also
passes an explicit-marker build overlay. No thresholds or approvals changed.
The final-readiness pause answered session/prompt with the non-standard
stopReason "error", so a client that reads only message text took unverified
work for finished. The turn now ends with end_turn, the typed outcome in _meta
and a host-written [paused] line at the end of the streamed text.
Problem: newer Studio introduces the model-facing setting tool across the
same catalog, boot assembly and controller used by the persona feature.
Root cause: validating only the earlier base would leave their combined
authorization and request contracts untested.
Fix: normally merge Studio 4840df982 and retain the existing guarded persona
entries, user-only loading and supplemental prompt assembly. Add a real
boot.Build effect test for all three persona fields: even in YOLO a model
proposal receives a guarded refusal, produces no approval card, preserves
the user file and discloses no value through refusal or settings listing.
Verification: gofmt, vet, lint, make check, full root Go tests with atomic
coverage, Desktop Go and SDK suites, frontend typecheck/build and 654 files
/ 5384 tests; Chromium settings navigation, row layout and font-scaling
checks in light/dark and wide/narrow layouts. Earlier Electron checks apply
unchanged. No baselines, thresholds, workflow approvals or draft state change.
Problem: the approved persona head failed hosted Go tests and coverage, and
the combined Settings/config files exceeded their line budgets.
Root cause: CI tested synthetic merge 6309314327 against newer Studio
5e53be32a. Its attachment ceiling key had no settings declaration, while
independent additions accumulated in the shared settings/config files.
Fix: normally merge Studio 667f2ca16, including the upstream guarded
attachment-key declaration and ProviderEntry method extraction. Keep all
three guarded persona keys and their user-scope/prompt behavior. Extract
the unchanged settings navigation into SettingsNav, retaining keyboard
navigation, status values and the new version dot. Assert that the
attachment storage ceiling stays guarded.
Verification: gofmt, vet, lint, make check, complete root Go tests with
atomic coverage, independent Desktop Go tests, Electron tests, frontend
typecheck/build and 654 files / 5381 tests. Chromium fs-scale and settings
row guards pass, with light/dark 1280px/390px navigation checks. No budgets,
test thresholds, workflow approvals or draft state are changed.
stopReason "error" is not in the ACP prompt-turn enumeration, and the cause
travelled only on a vendor notification, so a text-only client saw an
unexplained failure. The turn now ends with -32603, the redacted cause as the
message and data.code turn_failed.
Problem: remote sidebar mutations could dial an unconfigured SSH target,
accept unbounded JSON, and enable hover actions without a user choice.
Root cause: a missing host resolved to an empty workspace without an error;
remote JSON handlers lacked a shared size boundary, and the preference
treated every value except explicit off as enabled.
Fix: classify unknown hosts before link reuse or acquisition, share a
64 KiB single-body decoder across remote writes, and restore explicit-on
hover preferences with a safe storage-error default. Preserve configured
hosts, far-kernel refusals, menu access and declared workspace resolution.
Align synchronization fixtures and keep translated directory prose in the
interface font.
Verification: failing-first unknown-host and oversized-body regressions;
full serve, builtin and boot tests; 5389 frontend tests; production build;
gofmt, root go vet, make lint and make check; isolated browser interaction
checks for default-off, opt-in persistence, menu access and opt-out.
Problem: The SSH project menu re-derived a default from the project list when
the host response omitted workspace.
Root cause: A client fallback duplicated the kernel's configured-default rule.
Fix: Pass the host response's workspace directly to the menu. Align the fixture
with the server response and cover both an explicit default that differs from
list order and an omitted default that must stay unset.
Verification: The omitted-default regression failed before repair. All 17
remote project/worktree interaction tests and the production build passed.
gofmt, root go vet, make lint, make check and builtin/boot tests passed.
The key landed with the streaming attachment ceiling without a class, so the
settings package panics at registration and every test importing it fails.
config.go is over its file-size budget on studio after two merged changes,
which fails lint for every new pull request. The model-price/override and
API-key methods belong with ProviderEntry; the one function that reads the
process environment stays in config.go so no process-input debt moves.
Problem: the Studio browser guard on the persona PR measured file and
change counts staying at 10px when interface text was enlarged.
Root cause: both explorer headers share a small element without an explicit
font size, leaving the size response to the browser's smaller-text rules.
The stylesheet is inherited from studio; macOS Chromium did not reproduce
the Linux hosted failure.
Fix: give the shared counter rule an explicit 10px base multiplied by the
existing interface scale. Preserve the default appearance and the persona
and composition behavior. Add a bilingual release note.
Verification: gofmt, vet, lint, make check, builtin/boot tests, frontend
typecheck, production/perf builds and 651 files / 5370 tests; Chromium
fs-scale, text-size and explorer guards. In light and dark, both counts
measure 10px, 11.5px and 13px across the three interface size steps.
The metadata list mapped entries to an unkeyed fragment, so React warned about the missing key when Settings -> Extensions rendered a skill's spec. Give the fragment the entry key and add a regression test that fails without it.
Problem: the new studio settings catalog rejects config fields without an
explicit classification, including the three persona fields added here.
Root cause: the catalog foundation landed after the persona implementation.
Fix: integrate studio and classify persona prefix, suffix and optional runtime
context as user-owned Guarded settings. Plain file writes do not rebuild a
runtime. Reject model proposals before any persona value can be changed.
Verification: root Go tests including settings, boot and builtin pass; gofmt,
vet, lint and make check pass; frontend typecheck/build and 5,370 tests pass.
Problem: project configuration could replace the user persona, and complete
mode could suppress built-in guidance. The UI misstated rebuild semantics.
Root cause: persona was absent from the user-scope hold, replacement was a
supported runtime option, and settings metadata described only new sessions.
Fix: hold and clone the user's persona across project decoding, retire complete
throughout config/runtime/API/UI, and describe next-build activation. Integrate
current studio without dropping its settings or transport contracts.
Verification: gofmt, vet, lint, make check, root and Desktop Go suites, boot
scope/prompt regressions, frontend typecheck/build and 5,369 tests pass.
Problem: Host reads could borrow another pane's workspace or reuse an existing
link after its host was disabled. Review also requested explicit defaults,
boundary regressions and removal of development notes from the tree.
Root cause: remoteLink returned the first host pane before reading current
user configuration, so neither workspace identity nor the disabled policy
governed the reuse path.
Fix: Resolve the user-host default before reuse and accept only a matching
host/workspace pane. Share the declared-default-first rule with host views,
show actual and default connection paths separately, and document the rule
in both GUIDE languages. Remove three root QA notes and seven captures;
preserve their useful context in the PR discussion.
Verification: Reproduced both incorrect read behaviors before repair. Added
HTTP regressions for declared defaults, fallback, empty configuration,
reordered extras, explicit opens, project injection and disabled hosts with
existing panes. Focused frontend tests (80), production build, owning Go
serve/config and builtin/boot tests, gofmt, go vet, lint and make check passed.
A stored reference carried a path the adapter opened as written, so a session
file that was edited, restored or imported could name a file no tool had
checked. read_video now records the path it verified under the file's hash in a
process-local registry; the adapter resolves through it and reports an
ungranted reference as video.file_unavailable.
Problem: The unified sidebar conflicted with newer Studio worktree grouping
and text scaling changes.
Root cause: Both branches evolved project identity and row presentation from
the same baseline, including helpers extracted by the sidebar change.
Fix: Preserve workspaceContains across grouped local and SSH projects, keep
isolated session directories in open and palette routes, retain isolation and
peer-thread markers, and preserve font scaling. Include reviewed synthetic UI
captures and a release note for the relocated controls.
Verification: The integrated frontend suite passed 5381 tests. Four focused
grouped SSH/palette regressions and the production build passed. gofmt, go vet,
make lint, make check and builtin/boot tests passed without widening baselines.
Account caught with String(e), so a coded refusal such as
account.signin_disabled printed "Error: " and the kernel's English
fallback instead of the wording kernel.ts already has, and its
expired-request message was set without t() and had no English. The
port's accountPoll also hand-rolled its fetch and threw the raw
response text, a JSON envelope with no code to word.
Catch with reason(e) like every other panel, put the expiry message
through t() with an English entry, and send accountPoll through post0.
Problem: Machine-grouped navigation made project switching cumbersome, and
session hover actions had inconsistent selection and keyboard behavior.
Root cause: Local and remote lists owned separate presentation and opening
flows. Remote defaults, paused stream costs, mutation deduplication and focus
restoration did not consistently survive asynchronous navigation.
Fix: Present one project list with source metadata, a common project picker,
fold-all control and bounded recent-session previews. Share neutral selection,
leading pin and trailing archive actions across local and SSH conversations.
Route new sessions and quick switching through the active environment, retain
latest navigation intent, and guard pending mutations and keyboard focus.
Forward remote session flags to the owning kernel and preserve typed errors,
configured workspaces and dedicated-stream connection costs.
Verification: 5263 frontend tests passed, production TypeScript/Vite build
passed, and focused browser and real SSH navigation checks are documented in
the QA reports. Owning Go serve/remotehost/attach suites passed. Pre-commit
gofmt, go vet, make lint, make check and builtin/boot tests passed without
widening the repository lint baseline.
Problem: ordinary binary uploads can consume unbounded workspace disk, and the
review needs direct coverage of file versus directory grants at read consumers.
Root cause: the streaming saver copies until EOF without a byte policy; the
project-config merge must not become an authority for a user disk safeguard.
Fix: enforce a named 1 GiB default while counting real bytes, retain rooted
cleanup on overflow, and return typed ceiling/source facts. Allow user config
to set the limit or explicit zero, hold it across project merges, render it
only at user scope, and strictly read it without degraded config fallback.
Cover selected-file siblings and directory link escape through actual reference
and tool consumers. Incorporate current Studio without rewriting history.
Verification: the former 8-byte/9-byte acceptance fails first and now refuses
cleanly. Full config race and focused control/serve/builtin races, frontend
tests/build, actual Electron rejection/retry, native Windows ARM64 boundary
and cleanup regressions, pinned lint and repolint pass.
Problem: the attachment PR conflicts with Studio's extracted composer helpers
and the newer worktree setup surface.
Root cause: both branches changed the Composer's chip type and helper ownership.
Fix: merge origin/studio, use its shared composerChips owner and worktree setup
flow, and retain cancellable attachment uploads and native path references.
Remove the redundant local chip type instead of duplicating the shared owner.
Verification: TypeScript checking, 155 focused frontend tests, production build,
attachment/CSRF/cloud/control/fileutil Go regressions, 169 Electron unit and
packaging tests (one existing conditional skip), pinned lint and repolint pass.
Review follow-up: the audit helper used unix.Flock with no build tag, so lint's
typecheck and test (windows-latest) failed with 'undefined: unix.Flock'. Take the
lock through internal/base/filelock's TryAcquire, which has the same unix and
windows implementations the store uses, so the held-lock tests run on every platform.
saveNetwork, uploadWallpaper and clearWallpaper hand-rolled their fetch
and threw a plain Error, so the code the kernel sent never reached
reason(): an unsupported wallpaper read as the kernel's English fallback
in a Chinese window, a failed clear as a path and a status, and the
existing wording for network.rejected and wallpaper.* went unused. They
now go through post0 and del, whose fail() keeps it.
SaveNetworkSettings also stops prefixing its refusal with a Chinese
sentence: the frontend frames network.rejected itself, and the prefix
would read twice in one language and as a fragment in the other.
SsePort still declared exportPlugin, dryRunHook, trustMcp, reconnectMcp,
parseMcp and probeProvider after they were split into its faces, and a
method on the outer class hides the face's. So the faces' dryRunHook and
probeProvider, which go through post0 to keep a refusal's code, never
ran: probeProvider threw the raw response text, and onboarding showed a
refusal envelope as its message and could never offer the vendor-console
link it keys on provider.probe.unauthorized. exportPlugin was the other
way round, the face holding the stale copy.
Drop the six copies, move the current exportPlugin into SseExtensions,
and guard the chain so no face redeclares one below it.
renderSolveProfiles folded every run that reached a correct checkpoint into
the boundary medians, including runs whose trajectory was never read. With
-checkpoints and no -trajectories those runs carry zero round and
verification tallies, so the report printed "rounds before 0 · rounds after
0 · verifications after 0" as if they had been measured.
Only runs whose split was actually recorded now fold in, and a suite where no
run recorded one says the boundary was never split instead of printing zeros.
The all-measured medians are unchanged.
Problem: creating a worktree dropped keyboard focus when the composer became
inert, returning to editing did not restore it, and terminal pages still
reported waiting or mapped cleanup failure to a confirmation state.
Root cause: draft restoration attempted focus before React removed inert;
the preparation page had no focus owner, phase fallbacks conflated terminal
states, and details/edit actions shared a configuration identity.
Fix: focus the preparation heading on entry, recover focus when a removed
phase action leaves the body active, preserve surviving detail focus, and
restore the composer only after the stage-clearing commit. Use distinct
view action IDs and explicit terminal labels, and show elapsed waiting only
for active phases without changing existing typography or button recipes.
Verification: failure/cancellation interaction regressions, phase/disclosure
unit tests, real-browser focus and draft-retention checks, English dark-page
labels and shared button hierarchy, frontend typecheck/build/census, and
repository lint/check; full frontend: 634 files and 5,257 tests passed.
No Git or backend behavior changed.
setShellKillTree on Windows ran its own taskkill /F /T inside
exec.Cmd.Cancel with no time limit. #13323 bounded the taskkill in
proc.KillTree because Wait does not return until the cancel hook has,
but this hook never went through KillTree, so a PDF text extraction past
its 8 s deadline could still hang on a taskkill that never answers.
Delegate to proc.SetCancelKillsTree, the helper proc.RunCommand already
uses, so the only taskkill left is the bounded one in proc.
Review follow-up: Partial now accumulates across legs (a killed first leg plus a
complete second leg is still a lower bound), and the meter comparison skips a
Partial run the same way it skips an unaccounted one, since its missing tail is
not a divergence. Rebased onto current studio; the stub tests use requireShellStub
so test (windows-latest) can run.
Problem: hosted atomic coverage intermittently lost the MCP resource tool,
and CodeQL reported an unchecked filesystem probe during worktree cleanup.
Root cause: a queued cache callback read clients after Host.Close cleared
handshake metadata. Cleanup accepted a result path without independently
checking the trusted managed root used by its construction owner.
Fix: snapshot advertised capabilities and instructions before releasing the
deferred spawn; keep only persistence asynchronous. Capture the managed root
once for creation and rollback, validate a local relative descendant before
probing the filesystem, and retain registration and unchanged-head checks.
Verification: deterministic shutdown/cache and wrong-boundary regressions;
plugin atomic coverage and race tests; boot atomic coverage; worktree/serve
race checks; native Windows rollback cases; local CodeQL path query; root
lint, Windows lint and changed-file repository checks.
A leg whose final record was missing but whose <metrics>.partial snapshot was
read was marked Unaccounted, which gatherSuiteStats skips outright. That
contradicted Partial's meaning — real numbers that stop at the last snapshot
and belong in the totals — so the recovered tokens, cost and lower-bound hint
were all unreachable. Only a leg with neither record is now Unaccounted; a
recovered snapshot stays accounted and sets Partial, matching the semantics
swebench mode already uses.
renderMeterAccounting also treated PromptTokens+CompletionTokens == 0 as an
absent harness measurement, inferring state from a value. A persisted record
saying zero is a valid zero, so the guard is now the explicit Unaccounted flag
alone.
Tests: the copied audit case that asserted unaccounted=1 for a sidecar-recovered
run encoded the wrong semantics and now asserts Partial=true, Unaccounted=false
and that 1200/80 and the cost reach the aggregates; the divergence fixture that
relied on zero tokens sets Unaccounted explicitly, and a new case pins that a
persisted zero is priced on both sides. The record-less case and the healthy-run
control are unchanged in intent.
Problem: failed or cancelled worktrees remain registered on Windows, and the
cleanup-failed screen leaves users without a concrete recovery procedure.
Root cause: native backslash paths are compared directly with Git porcelain's
slash paths. Cleanup uncertainty deliberately fences replacement creation.
Fix: normalize both registered and expected paths while preserving symlink
resolution and the unchanged-head guard. Cover native rollback and changed
branches, wire the Windows regressions into PR CI, and explain draft retention,
manual reconciliation, runtime closure and retry from the source folder.
Restore the existing design QA content and metadata, append only workspace
acceptance, and remove all eight screenshots from the final PR tree.
Verification: native Windows regression fails before the fix; the fixed worktree
package passes 36 tests with 6 platform skips. macOS worktree suite, focused
serve/worktree race tests, Windows lint, workflow contracts, 5,245 frontend tests,
typecheck/build, census and repository lint pass. Hosted CI still needs approval.
Problem: Windows smoke reported an empty live catalog in the disabled-MCP
effect test's fresh always-loaded phase; the fixture expected one read tool.
Root cause: Build returns while cold discovery and always-loaded preconnection
can still be in flight. The fixture immediately issued an explicit connection,
leaving competing handshakes and initial catalog publication in its setup.
The adjacent always-load effect tests already await the boot-owned connection.
Fix: Wait for that connection in cold and always-loaded phases before explicit
registration. Keep cached deferred startup explicit and retain all filtering,
restart, catalog and provider assertions. Report held tools and server catalog
diagnostics if the allowed tool is still missing.
Verification: Related disabled/always-loaded/held-definition effect tests pass
normally and under the race detector. go vet, make lint and make check pass.
Full builtin and boot suites pass as well.
Native Windows CI must confirm the replacement candidate.
Problem: an explicit source refresh can repopulate the palette after switching
workspace, port or session, and an older initial read can overwrite a later read.
Root cause: only the initial fetch had a lifetime fence; manual reads published
without a shared generation. The browser guard still awaited a retired hover card.
Fix: put initial and manual reads behind one latest-read generation, invalidate
it on scope changes, and verify the unified palette before and after task delivery.
Verification: four deferred-response regressions fail without the fix and pass
with it; 633 frontend files / 5,245 tests pass, typecheck/build, census invariants
and fixture verification, repository checks, and browser palette interactions.
Upstream workflows remain pending repository-admin approval, not passed CI.
Problem: the PR overlaps the current Studio font-scaling update.
Root cause: both changes replace the workspace stylesheet, and hub additions
bring its combined source over the repository file-size ceiling.
Fix: retain the new palette with shared, scalable typography tokens; preserve
all upstream behavior and extract hub close/shutdown ownership together.
Verification: 633 frontend files and 5,241 tests; typecheck/build; affected
worktree, serve, control and boot suites and race tests; Desktop Go suite;
make lint/check; fresh production browser palette with no console errors.
Problem: workspace configuration disappeared after the first task and creating
an isolated worktree left the composer without visible progress or recovery.
Root cause: workspace selection, Git branch switching and runtime construction
had separate UI lifetimes, with no cancellable preparation/publication boundary.
Fix: keep one searchable workspace palette across conversation states; expose
real preparation phases with operation-ID reconciliation, retry and cancellation.
Serialize cancellation against publication, terminate checkout descendants and
clean unpublished resources before acknowledging cancellation. Preserve task
and attachment drafts, and reuse existing typography throughout status screens.
Verification: 5,231 frontend tests, typecheck and production build; root and
Desktop Go suites; affected backend race tests; lint and repository checks;
real Git browser checks for switch, failure/retry, cancellation and handoff.
Include light/dark and wide/narrow before-and-after screenshots and design QA.
Problem: CodeQL still reported a path-injection alert at the worktree
registration read, and a registration symlink could read outside its metadata
directory before assigning the checkout to a project.
Root cause: Validating checkout identity and the registration backlink did not
constrain the filesystem operation that reads that backlink. The plain
os.ReadFile call followed symlinks outside the selected Git directory.
Fix: Validate the checkout metadata path relative to the shared repository,
open that directory through os.Root, and read the fixed gitdir entry through a
second root confined to this checkout's metadata. Preserve relative backlinks
and internal symlinks while refusing escaping registrations.
Verification: A real-Git symlink-escape regression fails on the original code
and passes with the fix. Worktree/workspace identity suites, grouped-project
HTTP regressions, focused race tests, Windows test compilation, go vet,
make lint and make check pass. Local CodeQL CLI 2.27.1 with go-queries 1.6.11
reproduces the exact remote alert before the change; afterward that alert is
removed and the same path-injection query introduces no new results.
Builtin and boot integration suites also pass.
Problem: Checkout labels could push a conversation title beyond its breadcrumb
at phone widths and high interface scale.
Root cause: The fixed-width directory badge consumed the title's reserved
space even after project and folder controls were hidden by the narrow layout.
Fix: Group project and checkout context in a shrinkable clipped flex child,
allow labels to ellipsize, and preserve the title's existing four-em floor.
Document the shared layout constraint without weakening browser guards.
Verification: The eight failing light/dark width-and-zoom cases now pass in
perf/narrow.mjs. Title spacing and overflow, intrinsic layout and hover guards,
12 related interaction tests, typecheck, production build, vet and repository
checks pass. The Go implementation is unchanged by this layout correction.
Problem: Grouped projects could open the source after an explicit worktree
selection, hide source-alias history, omit inferred launcher exclusions, or
remain unusable when their source folder disappeared. Copied Git metadata
could also claim another checkout's project identity.
Root cause: Display identity was reused as execution and catalog authority.
Project inference duplicated an unbounded marker walk without validating
managed checkout containment or the reverse Git registration.
Fix: Preserve selected directories and all catalog members, choose a surviving
project directory, and use complete member roots for editor discovery and
launch. Reuse shared checkout identity and validate its managed boundary,
local prefix and registration backlink. Merge studio, move root resolution to
its project owner to preserve the file-size budget, and align the title guard
with the intended checkout label while retaining spacing and overflow limits.
Verification: Failing original-head HTTP/path regressions pass after the fix.
Frontend tests (629 files, 5,221 tests), production build, focused race tests,
go vet, make lint and make check pass. Added a 100-root tree benchmark and a
deterministic delayed-header test; repository suites and browser guards cover
adjacent behavior. Hosted checks must validate the replacement PR head.
TestCredentialSinkRegistry walks internal/frontend/serve, where serve's
own tests create and remove extra-* folders through tempAside. Run in
parallel with those tests, the walk can list such a folder and fail when
it is gone a moment later, the same race that failed the storage
state-path guard. Skip a vanished entry with testenv.VanishedMidWalk.
Problem: Studio lists a managed worktree and its source checkout as two
same-named sidebar projects, scattering one project's conversations.
Root cause: Sidebar ownership, session catalogs, and project operations use
checkout paths as project identities even when they share a Git repository.
Fix: Resolve project ownership from managed checkout Git identity while
retaining each transcript and runtime working directory. Merge session
catalogs before paging and keep registration, ordering, capacity, and removal
consistent with the project group. Show the checkout type in the chat header
and mark isolated history without migrating persisted data.
Verification: Serve/worktree/workspace identity and builtin/boot suites,
focused race tests, Desktop Go tests, frontend tests and production build,
go vet, make lint, and make check passed. Browser interactions and an installed
macOS package verified grouping and original/isolated conversation navigation.
GET /workspace/file takes an optional root, checked against the grant list and re-vetted on read through the decision the picture route uses. The workbench carries the folder, never saves, and says why.
A queued message that froze an image cannot be admitted as mid-turn guidance. The manifest now records that (images), and the queue row says so instead of 'not delivered yet'.
recordStalled emitted a notice with no code, so the frontend showed the kernel's English sentence as is. It now carries job_stalled and a JobNotice payload with the quiet span, like the other job notices; the English text stays the fallback.
Hosted Web enables the multiplexed event stream for every pane, but a
Studio kernel without GET /events/mux answers it with the SPA shell. The
mux client waits for the stream's open frame to name its connection, so
no pane ever subscribed and none fell back to its own stream: the page
kept loading and stopped receiving events until a reload rebuilt it from
a snapshot.
RemoteEventSource is a polled stand-in that cannot carry that protocol,
so a hub reached over the relay now reads each pane's own /events. A
local hub keeps the shared stream.
The event contract is unchanged: attach, resume, cursor, watermark and
the pending-prompt attach all keep the code paths PR #13499 fixed.
Problem: CodeQL reports uncontrolled paths in streamed upload cleanup and
attachment creation. A replaced storage directory can redirect failed-upload
cleanup to an external same-name file; a linked .reasonix parent can escape too.
Root cause: filename validation and path-based writes/removals do not preserve
directory identity across filesystem changes during I/O.
Fix: use os.Root for workspace-contained directory/file creation, share an owned
file lifecycle between streamed and legacy byte writes, and retain the directory
handle for failure cleanup. Publish the existing untracked marker atomically
through the same rooted directory rather than reopening its pathname.
Verification: full control/serve/fileutil suites, focused race tests, vet,
pinned linter and repolint passed. Native Windows 11 ARM64 regressions passed
with directory junctions and protected cleanup, plus large bounded streaming.
The independent replay test failure also reproduces with the PR's Go changes
removed through a base-source overlay; no replay behavior or CI gate is changed.
Fold the fragments merged after the first notes commit into 2.43.0.md and remove every consumed fragment, so the release commit leaves release-notes/studio/unreleased empty.
Problem: Review requested a user-visible release fragment and clearer memory
bounds. The live regression also assumed new-session activation must create
a second runtime, which is false when an existing blank pane is reused.
Root cause: A workspace may already own a reusable blank pane, particularly
when native Windows roots have matching canonical spelling. The old setup
waited for a POST and a new pane identity instead of editor readiness.
Fix: Wait for boot completion at a stable viewport, allow a blank-pane reuse,
and preserve assertions for the captured workspace, one shared intake route,
saved upload size and four ready attachments. Allow an explicit test host
binary. Add bilingual release notes and state that memory, not total disk
consumption, is bounded by streaming.
Verification: Windows 11 ARM64 control, stream, HTTP upload/image-limit and
exact-file read-tool regressions passed. Electron 44.2.0 exercised picker,
paste and drop of a 111 MiB file and a 65 MiB streamed File with four ready
attachments. Release-note tests, repository checks and script syntax passed.
Problem: The full frontend suite rejects the upload abort listener as an
unresolved interaction registration.
Root cause: The syntax-based census does not infer a Request.signal member's
AbortSignal type and cannot distinguish it from a possible user-input target.
Fix: Keep an explicitly typed upload signal and teach the shared receiver
certifier to recognize scoped AbortSignal bindings. Preserve unresolved
receivers and refuse borrowing an outer signal type across shadowing.
Do not add a file exception or raise any census or lint ceiling.
Verification: Abort receiver, actions census, cloud upload and attachment
regressions, frontend typecheck, production build and repository checks pass.
Problem: Large ordinary file attachments cause avoidable whole-file
allocations and exceed the legacy JSON attachment transport limits.
Root cause: Picker and paste intake Base64-encode files even when the shell
already knows their native paths. Pathless uploads also pass through JSON.
Fix: Share native path references across picker, paste and drop. Stream raw
ordinary-file bodies to bounded disk writes and clean up failed or cancelled
uploads. Grant external-file access only to the selected file. Preserve
image, legacy JSON and cloud relay limits; bound cloud intake before
materializing an oversized body. Document the transport and live regression.
Verification: Owning Go package suites, focused race tests, go vet, lint,
repository checks, frontend typecheck, production build and attachment
regressions passed on the original tested baseline. Real macOS Electron
exercised native picker, paste and drop with a 111 MiB file and an exact
65 MiB pathless upload. Windows native runtime verification remains open.
A window that gives each pane its own EventSource spends one HTTP/1.1 connection per pane, and a browser origin holds six, so the sixth stream starves every stop, submit and read behind it. GET /events/mux is one stream for all panes of a window; PUT/DELETE /events/mux/{conn}/panes/{id} subscribe a pane with its own cursor. /events and /events/replay are unchanged.
RemoteEventSource now leaves the attach cursor empty so the kernel hands a late client the pending prompt. The tree stream shares that class but reads the tree rather than history, so an empty attach for it re-read the tree on every poll and failed tree_events.test.ts. The attach query is now chosen once from the path: the main stream attaches with no cursor, the tree attaches past the watermark as it always did.
Verified with the whole frontend suite: typecheck, 5139 tests, census, census:verify, build, and no fixtures in the bundle.
Home declares no entries, so the survey treated it as sole occupant of its
directory and counted the sessions and archive that state declares in the
same folder (one folder on a default Windows install).
The cost chip drew a literal "¥" default, so a fresh session read
"本轮 ¥0.00" in a currency nothing had priced, whatever the display
currency was. Leave it out until the first priced round or restored total.
The polled attach took its pending-prompt snapshot under promptEmitMu but read the broadcaster watermark after that lock was released. A prompt raised in between was numbered at or below the watermark the client then adopted as its cursor, so the next resume skipped it — the original bug in a narrow window.
The watermark is now read in the same critical section as the snapshot, and before it: a prompt raised after that point is numbered above the cursor the client adopts, so the worst case is a prompt named twice rather than one lost. writeEventsReplay reads the watermark once and passes it in, instead of the writer reading it after the lock.
The shell's no-store comment is trimmed to the comment budget so the lint gate passes.
The argument completion of /goal, /memory, /mcp, /skills, /plugins, /theme,
/currency, /reasoning-language, /ask-timeout and /feedback returned English
literals; read them from the i18n catalog like /language and /effort do.
TestEveryTopLevelStatePathIsAnOwnedEntry walks every package under
internal/ and failed the moment WalkDir could not open an entry. The
serve, control and config tests take their extra folders from their own
package directory (tempAside), so when go test runs packages in
parallel the guard can list one of those folders and find it gone a
moment later:
open .../internal/frontend/serve/extra-3867033970: no such file or directory
The config package's state-root walk already tolerates this through
vanishedMidWalk, including the sharing-violation and access-denied
errors Windows gives for a directory being deleted. Move that helper to
testenv as VanishedMidWalk, unchanged, and use it from both walks.
TestCreateFromBranchKeepsSourceAndSeparatesTasks required the checked
out README to be exactly "base\n". Git for Windows ships core.autocrlf
true in its system config, so the worktree holds "base\r\n" and the
test failed on Windows CI. Trim it as the sibling dirty-source test in
worktree_test.go already does; the check still tells the selected
base apart from the current head and the dirty source.
TestAuditLegacyCacheUpgradeReportsOnlySuccess blocks the legacy-key
upgrade write by chmod-ing the cache dir to 0555. On Windows that only
sets the read-only attribute, which does not stop new files in a
directory, so the upgrade reached disk and the test failed at its own
fixture check on every Windows CI run since it landed.
Deny write to Everyone with icacls there instead, the mechanism
internal/contract/config's makeDirReadOnly already uses.
Problem: Studio CI caught ArrowDown selection returning to the default row
before Enter could confirm the selected reference.
Root cause: candidates rendered before a passive effect reset selection and
keyboard ownership. That later update could overwrite navigation after the
rows had already committed.
Fix: store the completion answer, active row, and keyboard ownership in one
state value. Publish each answer with its default selection and remove the
deferred reset. Guard navigation against an answer that has become empty.
Verification: four deterministic reference/slash publication and early
navigation regressions fail on the old hook. The PR merge snapshot with this
patch passes 616 frontend files / 5,132 tests; 92 completion/composer cases
pass with --repeats 10. Typecheck, build, census and census verification,
go vet, make lint, make check, and builtin/boot tests pass. Chromium confirms
ArrowDown plus Enter accepts the selected reference and slash command.
Problem: Review requested proof that production completion removes exact
reference candidates independently of the frontend MockPort fixture.
Root cause: The frontend interaction tests and the existing control unit
test did not exercise the registered GET /complete route together with its
controller data, scoped filesystem lookup, and UTF-16 boundary.
Fix: Add seven real HTTP cases using the production handler and disposable
workspace files. Cover partial paths, exact filenames, prose and emoji
prefixes, escaped nested paths, retaining a genuine longer alternative,
and an exact final alternative. No production filtering behavior changes.
Verification: The completion HTTP tests and full serve package pass, along
with go vet, make lint, make check, and the builtin/boot pre-commit suites.
Removing only the existing control.dropTyped call via a temporary Go build
overlay makes six HTTP subtests fail on the retained identity insertion;
this checks regression sensitivity and is not a production defect claim.
Brings the three Web fixes onto the current studio line: the hosted page's own-fetch stale check, the pending approval/ask handed to an attaching polled client, and the no-store shell.
The one conflict, internal/frontend/serve/web_entry_test.go, was both sides appending a test at the same point; both are kept.
Problem: Review requested coverage for Enter after a manually completed
reference, including the case where another candidate is still available.
Root cause: The kernel already removes an insertion equal to the typed
token, but MockPort kept that row and manufactured an extra confirmation
step in development fixtures.
Fix: Keep MockPort's candidate filtering aligned with the kernel and add
four composer regressions. A full path with no remaining candidates sends
in one Enter. An open menu containing a genuine alternative still confirms
that alternative before submission, as required by the interaction contract.
Verification: The three exact-path cases failed against the old fixture.
All 5099 frontend tests, typecheck, build, make check, go vet, and make lint
pass. The existing kernel dropTyped regression passes. Real-browser checks
confirm single-Enter submission for exact paths at wide/light and phone/dark
sizes. The builtin and boot pre-commit suites pass.
A read-only tool that turns a video into a few time-labelled pictures for a
model that reads images. The container is named from the file's own bytes and
ffmpeg is forced to it with file-only protocols; failures carry typed codes.
It is reached through use_capability and stays out of the schema prefix (#13399).
The polled transport attached past the watermark and dropped what came back, so a page opened after a prompt was raised never saw the approval or ask the run was waiting on. An attach now carries the pending prompt to that client alone, and the client reads the frames it is sent.
A hosted page checked for a newer build through the global fetch, which remoteHub has already replaced with a tunnel to the kernel; it compared its own build against the kernel's and reported itself behind forever. The check now reads through the page's own fetch, bound before the swap.
The shell names which build is current and was served with no cache policy, so a reused copy left an open page behind and reloading landed on that same copy. It is now served no-store; the content-hashed bundles under /assets keep their long cache.
With an open-session limit set, opening one more session reuses or closes an
idle pane. A calm note now names the conversation, the limit, and that it is
still in the list; a note is a toast tone with no report button (#13419).
Problem: Pressing Enter while an @ file or directory candidate was visible
could submit an unfinished prompt instead of confirming the reference.
Root cause: The menu highlighted its first item but only owned Enter after
explicit navigation. Directory acceptance also shared the descent path.
Fix: Let the visible selection own Enter and distinguish confirmation from
Tab descent. Finish accepted references with a separator, preserve existing
whitespace and trailing text, and clear the old completion before drafting
continues. Update both locale hints.
Verification: Twelve new composer regressions reproduce the old behavior
and cover files, directories, nested Tab descent, pointer acceptance,
escaped paths, send preferences, Shift keys, IME, and Escape. The full
frontend suite passes (609 files, 5095 tests), alongside typecheck, build,
UI census, make check, go vet, and make lint. Browser interaction was
verified with the local MockPort workspace.
A button on a finished tool card repeats that call's own recorded card in a
section at the end of the transcript, as a view only: nothing is stored or
sent, and the original stays where it happened (#13394).
The transaction-free fallback loaded the disk manifest without the RunID
isolation the locked path applies, so a queue another run (or a previous run)
left behind could be dispatched while the lock was momentarily unavailable.
Refuse a foreign manifest with ErrForeignRun, which the dispatcher already maps
to a retry, and give the frontend a coded refusal for it.
NextQueued returns (InboxItemMeta, bool, error) and, when the disk
transaction is unavailable, reads the published manifest without the lock
(commits land by atomic rename, so that snapshot is complete). The
dispatcher maps a read error to inboxDispatchRetry instead of reporting
the inbox idle.
Narrowed per review: the permanent 1s long stop (armInboxDispatchLongStop,
its constant, its arming and its stop/reset wiring) is removed, so the
retry schedule past the existing bounded burst is exactly what it was
before this branch. The dispatcher audit case is rewritten to assert the
in-scope contract - a failed queue read schedules a retry and does not
report idle, and the durable item survives - rather than delivery after
the burst was exhausted, which was the removed policy.
NextQueued discarded beginDiskTransactionLocked's error and answered from
the in-memory snapshot, so a transaction that could not run — another
process holding the lock, EACCES, ENOSPC, a transient EIO — looked exactly
like an empty queue. dispatchInboxOnce read that as inboxDispatchIdle and
stopped with the durable item still queued, permanently.
NextQueued now reports that failure as an error, and before it will claim
the queue is empty it falls back to reading the published manifest without
the transaction: commits land by atomic rename, so that snapshot is
complete, and repair work stays behind the lock. A genuinely empty or
paused queue is still idle.
The bounded burst (three attempts, ~750ms) used to abandon work its own
scans had seen. Past the burst the dispatcher now parks on a slow
production timer that keeps re-checking until the item advances or the
queue reads empty; reset and Close cancel the pending timer.
A read error maps to inboxDispatchRetry, so an unreadable queue retries
instead of ending dispatch.
When an install already has frontend-next/dist beside the binary, upgrade
reads the page out of the verified archive, validates it in memory, swaps it
in before the binary and puts it back if the binary does not land (#13406).
ListSessionOrder keeps a row whose branch-meta sidecar cannot be read, so one
broken sibling cannot hide a folder, but every column of that row is then a zero
value. recoveryArchiveLineage consumed that row as 'not a recovery copy of this
conversation' and SetSessionLineageArchived reported success while a copy of the
row stayed active and listed on its own. The lineage walk now reads each listing
row's sidecar itself and refuses the transaction, so the archive endpoint
reports the read failure instead of a partial success.
ListBranches dropped a branch whose sidecar could not be decoded while a missing
sidecar built defaults and kept it; interruptedTurnContinuedOnRecoveryBranch
read that absence as 'the turn did not continue on a recovery branch' and
stripped the interrupted turn's messages. An unreadable sidecar is now handled
exactly like a missing one.
Problem: release notes did not mention the focus behaviour shared by all
narrow composer toolbars, making the compact empty state look accidental.
Root cause: the behaviour and pointer stability rationale were documented
only in the internal style contract.
Fix: add matching Chinese and English release-note entries explaining that
empty narrow toolbars remain compact on focus and expand for draft text,
attachments or an open menu, keeping pointer clicks on the same button.
Verification: 12 release-note rendering and Markdown reference tests passed;
make check and git diff --check passed. Documentation-only change.
The cli job builds the page once and packs it beside the executable under
frontend-next/dist, where serve.FindPage looks, through a packer and a checker
that are run by the job and by tests (#13406).
Problem: the five composer menus appeared oversized beside the application's
compact toolbar and other menus.
Root cause: the shared composer menu style imposed 15px labels, 13px notes,
380px panels, 20px corners and generous spacing on every menu. Workspace
toggle and note borders also produced adjacent divider lines.
Fix: use 13px labels and 11px notes, 12px corners and 8px panel insets; choose
280-340px widths by content. Tighten rows, headings, search, chips, checks
and the workspace switch together, leaving one workspace note divider.
Keep all changes scoped to composer menus and update their style contract
and bilingual release note.
Verification: 22 related suites / 226 tests; production build; make lint,
make check and git diff --check. In-app browser checks cover all five menus
in light/dark themes, 390px and 320px viewport bounds, model/branch search,
Escape dismissal and focus restoration. Mock data only.
FindPage now also looks beside the file an executable link resolves to,
after the directory it was invoked from, so a package manager that links the
binary onto the PATH still finds the page next to it (#13406).
Problem: the workspace PR conflicted with the current studio composer, and
restoring its toolbar slot exposed narrow-pane overlap and pointer failures.
Root cause: the new side-question control shares the same toolbar; a fixed
model minimum could overrun its flex group. Focus-dependent phone folding
also moved buttons between pointer press and release.
Fix: retain both worktree setup and the upstream side-question button; allow
model shrinking and internal wrapping at the narrowest widths. Keep empty
phone toolbar geometry stable on focus and extend browser guards to cover
both workspace variants, pointer opening and nested button overlap.
Verification: 5,021 frontend tests; affected suites 126 tests after CSS edits;
production and perf builds; make lint, make check and diff whitespace checks;
hoverradius, branch-menu, modelrows and extended narrow browser guards;
light/dark worktree selection, Escape/focus, menu bounds and first send.
Run analysis and Workbench become icons on the window bar for the focused
pane; the Conversation / Run analysis / Workbench row, the plan peek and
their styles are removed (#13369).
Problem: workspace and branch controls were moved outside the composer, and
the footer-only 8px override broke the shared toolbar radius browser guard.
Root cause: the workspace slot bypassed the existing branch toolbar layout
and overrode the common mode button geometry.
Fix: restore both controls to the original toolbar slot, reuse branch layout
and pill styling, remove the footer overrides and keep runtime statistics
below the card. Cover new and existing conversations in the browser guard,
load the production menu stylesheet in the perf fixture and align the docs.
Verification: reproduced both layout assertions and the light/dark radius
failure before the fix; 4,831 frontend tests passed; production build,
make lint and make check passed; hoverradius, branch-menu, modelrows and
narrow browser guards passed.
A sticky bar of groups (all, new replies, needs your reply, open, closed)
with counts sits at the top of My feedback; rows that do not match are
hidden rather than unmounted so open threads and reply drafts survive (#13388).
The composer's plan fold now carries the /goal text when there is no plan,
and the three pane views get Ctrl/Cmd 1-3 shortcuts and palette entries.
Prerequisite for removing the top plan peek and view strip (#13369).
#13366 bounded the failed tool results a fold keeps, with component tests. This
adds the effect test through the real Build: 60 turns of failing commands fold
seven times, and the request right after a fold must not carry more failed
results than the retention budget plus the recent tail allow (4 with the bound,
9 without it, where the 50% ceiling is the only thing that clears them).
Refs #13148
runSegments read only the final metrics file. When a child was killed or
timed out it folded the missing record in as an accounted zero, deleted the
<metrics>.partial snapshot the sink had been writing, and left Unaccounted
unset, so every published total omitted what the run spent.
Read the sidecar when the final record is absent, keep the file it came from,
set Partial, and mark the run Unaccounted when neither exists.
renderMeterAccounting no longer prices a run whose harness reported no tokens
against the meter, which invented a -100% divergence.
LoadCachedSchemaForSpec rewrote a legacy-keyed entry in memory, discarded the
SaveCachedSchema error, and returned the hit with the new CacheKey. With a
read-only or full cache dir the file still held the legacy key, so boot was
handed a key that a later read cannot reproduce and pinned the session's MCP
tool identity to a key with no on-disk counterpart
(internal/assembly/boot/mcp_registration.go:76).
Keep the hit but claim the upgraded key only once the rewrite reached disk; on
failure the returned object keeps the legacy key the file holds, which matches
the package's best-effort, silent-degradation policy. A successful upgrade is
unchanged, and no caller can now observe a key the file disagrees with.
checkpointImages reported "no such image" for a checkpoint it could not
inspect, so refuseDegradedCheckpointRewrite read a stat failure, a read
failure, a non-regular file, or a file past the replay ceiling as "nothing to
protect" and let a transcript missing an image replace the only copy of that
image — the exact case where the guard cannot know.
Return the inspection error from checkpointImages, and have the guard fail
closed under ErrSessionImagesUnavailable for any checkpoint that exists but
cannot be read whole. A genuinely missing checkpoint still proceeds: there is
no second copy to protect. The restore path stays best-effort and now logs why
it could not inspect the checkpoint.
recordDispatcher.run discarded Writer.Append's error, so Flush — the only
success/failure signal the stats pipeline has — returned nil even when every
queued append failed. The dispatcher now keeps the first failure since the last
flush sentinel and hands it to the waiter through that sentinel, so a caller
that gates a deterministic read on Flush cannot mistake a failed write for a
durable one.
The failure is consumed by the flush that reports it, so a later flush over a
recovered directory returns nil, while a still-broken one keeps reporting. A
full queue may still drop a record; that stays documented and unchanged.
Tests: the audit red test plus controls for a healthy flush persisting records
and for failure-then-convergence.
A fold keeps failed tool results since the latest digest. The projection places
them right after that digest, so the next fold read last fold's survivors as
"since the latest digest" and kept them again: retention grew by every round's
failures and only stopped when the candidate crossed the 50% checkpoint ceiling.
Failed results now have their own retention budget (5% of the window, like kept
user turns), newest first; the rest fold into the digest, whose coverage guard
still names them. [[keep]] messages stay outside the budget.
Refs #13148
The covered-prefix hash includes the system message, so editing the system
prompt between two visits retires a saved compaction. #13336 added the notice
at the agent and control layers; this pins it at boot.Build, and pins that an
unchanged prompt stays quiet.
Refs #13295
Problem: concurrent tasks need separate code directories rooted at a chosen
branch, and composer configuration menus use inconsistent visual scales.
Root cause: isolated workspaces only started from HEAD and their entry was
outside the new-conversation composer. Menu surfaces had independent styles.
Fix: stage worktree and exact base-ref selection before the first submit,
carry draft attachments through confined roots, preserve session settings,
reject stale preparation and roll back unpublished worktrees on failure.
Keep existing conversations and older servers on the branch control.
Share composer menu surfaces, moderate 15px labels, 13px descriptions and
neutral selection states across mode, workspace, permissions, model and effort.
Move workspace controls below the input card beside runtime statistics, retain
branch labels in narrow panes, and wrap model/effort at the smallest widths.
Verification: 4831 frontend tests; TypeScript and production build; real Git
worktree/source/refusal and attachment confinement tests; serve, control,
builtin and boot suites; go vet, make lint and make check. Browser interaction
and light/dark responsive captures are kept outside the repository.
Fixes#13264
A file restore the checkpoint covers only partly stops for one more
answer, taken with Enter or y (the panel's own hint says so); the key
table in both guides listed neither the step nor y. The new test pins
that step: it does not commit before the answer, Esc steps back without
restoring, and Enter or y commits.
agentpreset.Normalize maps "light" and "economy" to balanced, and All()
lists balanced and delivery; light's own policy no longer exists. The CLI
still put light forward as a choice, in seven hand-written strings: the
--preset help of run, tui and serve, the --profile deprecation warning of
each, and parseRuntimeProfile's refusal ("want light, balanced, or
delivery"). The guides went further and described a Light policy (direct-
first planning, targeted verification) that nothing enforces, so a user
who picked it got balanced without being told.
The seven strings now come from agentpreset.All() through livePresets in
preset_flags.go, and the docs (GUIDE, GUIDE.zh-CN, CLI.zh-CN, TOOL_CONTRACT)
offer balanced and delivery, drop the Light policy bullet, and say the
retired names select balanced. light and economy still parse.
TestEffectThinkingTimeIsKeptWithTheTurn failed in the race job on studio
(run 38635511666): "thought kept 46 ms, framed 46 ms; want both at least
50". The scripted provider wrote the reasoning chunk into a buffered
channel and began its 60 ms pause at once, while the host times the
thought from when it takes that chunk; a host that reads the first chunk
late measures less than the pause. Returning the stream 30 ms late
reproduces it (29 ms measured).
The channel is now unbuffered, so the pause starts only after the host has
taken the reasoning chunk and the measured thought cannot be shorter than
it.
acquireLocal waited on the in-process token and ctx.Done in one select.
When both were ready (a lock nobody held, and a deadline already spent)
select picked one at random, so half the time Acquire refused a free lock
with the context error. The file-lock half of acquire already tries once
before it looks at ctx; the in-process half now takes an available token
first too. A held lock still fails at once once the deadline has passed.
This is the failure behind the unsealed evidence bundles on Windows CI.
TestPlanCriterionIsVerifiedByItsStepsCommands (studio run 38664267053)
reported "bundle not sealed: failure trusted_state.unwritable, contract
failure """ after 3.09 s: sealShadowBundle's two-second sealWait covers
the contract seal and object writes as well as the lock wait, so slow
writes spent it, and the bundle's own Append then met a free stream lock
with an expired ctx. With the first write slowed by 2.2 s, 1 of 6 turns
ended unsealed with that exact signature before this change, 0 of 6 after.
Keeping the decoded prefix across ANY decode error dropped every record after
a mid-file break, and that truncated prefix then competed as a checkpoint that
extends the replay. Damage before the last record now refuses the checkpoint.
recallTail had three outcomes and only two reached the audit channel: a
carried memory note and a normal recall each recorded a content-free
decision, while the retrieval ablation switch returned silently and left
lastRecall pointing at an earlier turn. A trajectory could not tell
"retrieval switched off" from "recall found nothing" from "suppressed by a
carried note".
All three now record a RecallResult and emit one memory-recall audit. The
identity travels as a typed memory.SuppressionCode on its own field, from
RecallResult through event.MemoryRecallAudit into the trajectory record's
suppression_code. The prose sentence stays exactly as it was, for readers
only: consumers match the typed field and never split the code out of the
text, so wording cannot become load-bearing again. The new field is additive
and omitempty, so existing records and batches decode unchanged.
The ablated turn also records lastRecall, matching the note path: the
diagnostic surface is defined as the last real turn's decision, and that
turn did decide not to retrieve.
reportBoundaries folded every row into byKey, so a run that never produced a
transcript re-entered the cue-boundary table as a zero measurement after the
funnel, dose and reformulation exits had excluded it. Skip failed rows, count
them, and say scored=0 with the failed count when nothing is left to compare.
A run that failed before its first model turn was appended as a zero-valued
contextMetrics, so results.jsonl gained an empty row, summarize counted it in
Scored and its stage tables, and batch.json reported it as completed with an
empty task id. Stamp such a run with its task, arm and NoEvidence stage, mark it
failed, and keep it out of every statistic while counting it in the manifest as
failed_runs.
load() dropped a turn file that existed but would not read, so a resumed
session numbered the next turn below it and Begin overwrote the only copy
of that snapshot. The store now reserves those turn numbers in memory:
NextTurn() counts them and UnreadableTurns() names them, while Bounds()
and List() still expose only readable checkpoints. No new on-disk state.
A persisted turn-N.json that cannot be read is dropped by load() exactly
like an absent one, so NextTurn() restarts below it and the next Begin
overwrites the only copy. This test operates on a real truncated file
written by the store's own persist path.
checkpointExtendingReplay read the .jsonl checkpoint with the strict loader
and folded a decode error into "this checkpoint does not extend the log".
The in-place append in appendSessionDisplayReadModel is exactly the write a
crash leaves half done, so the torn checkpoint's own intact lines were
discarded with it: resolveReplayedMessages then returned the shortened
replay with err == nil, and the next save rewrote both the checkpoint and
the log from that shorter transcript. Every turn the checkpoint still held
was lost permanently.
Decode tolerantly the way transformAndCopyJsonl already does, and keep the
existing prefix checks, so the tolerant result is adopted only when it
still extends the replayed prefix. A checkpoint that cannot yield a usable
prefix (unreadable, shorter, contradicting) keeps today's "does not extend"
behaviour; there is no third state.
Two gaps in the delivered-fact re-check this branch added:
- applyWrite kept the pre-reload stamps of the facts the model already
holds, so a change that one reload had settled was re-detected by every
later recall: the snapshot reloaded and the whole store was re-tokenized
on each turn until a recall returned hits. Re-stamp the kept facts
against the snapshot just loaded, dropping what it found gone, so the
steady state stays one read of each delivered fact's own file. The
pre-branch code cleared the set on reload, which is why this is not a
new promise.
- The remember tool's typed Memory was discarded at the queue: the note
carries prose and the controller must not parse it back into memory, so
a fact the model wrote itself was absent from the delivered set and an
external edit before the next recall served the stale session snapshot.
memory.Queue now carries the typed facts the tool wrote, remember passes
result.Memory, and the controller records it as delivered. The model's
own write still queues no turn-tail note.
Adds the delivery/change matrix (100 cells over delivery x change time x
change shape) with the reload-count companion, and the provider-boundary
regression for the remember path.
Problem: a fact saved through Controller.SaveMemory and then edited outside the
session is still recalled from the stale snapshot on the next turn; the per-turn
re-check that catches external edits compared each recalled fact with a stat, so
a rewrite that preserved the file's byte length and modification time stayed
invisible and the model kept reading the stale body; and the identity read behind
that stat, Store.Read, resolves a name by listing the whole store directory and
parsing every fact in it.
Root cause: the re-check set held only the previous recall's hits. A save queues
a turn-tail note, and a note suppresses recall for that turn, so a just-saved
fact was never a hit and so was never re-checked. Its check also rested on size
plus modification time, which is not semantic equality -- the repository's
context-projection rule says freshness detection must not rest on mtime.
Fix: baseline the facts the previous turn handed the model -- the previous
recall's hits, plus the facts a write note carried -- with a digest of each
fact's own file, read once at the canonical path its scope and slug name. Before
a recall tail is built those files are re-read directly and their digests
compared: different bytes reload the snapshot, identical bytes never do. The
stamp carries no size and no modification time, so no stat result can decide
that a fact is unchanged; the bytes are the semantic version because they are
exactly what the renderer turns into the model-visible fact. Store.Read stays as
the fallback for a fact whose file is not where its name says.
Cost: one read of each delivered fact's own file per recall, plus one when it is
baselined and, on that baseline, one parse of the same file to confirm the stamp
describes the delivered fact; a snapshot reload only when a delivered fact's
bytes differ or its file is gone. A formatting-only external rewrite re-reads the
fact into the snapshot even though the parsed fact is equal -- the safe
direction. A fact edited into relevance that the previous turn never handed over
stays invisible until the session reloads; no full scan, which
docs/SESSION_MEMORY_RETRIEVAL.md now states, along with the no-hit retention and
the identity fallback.
This extends the merged fix in 99fe3dbbf, which covers the recall-hit path only.
Review round 1 fixed two regressions in this baseline:
- A fact edited between the snapshot load and its first recall was tracked with
the edited file's own digest, so the stamp described bytes the turn never
carried and SameContent() then called the stale snapshot body unchanged
forever. stampFact now marks a fact tracked only when the one file it just
digested re-parses to the very fact being delivered (memory.FactAt + sameFact,
the comparison the untracked fallback already used); otherwise it records the
fact untracked and the identity read decides. The probe stays on that one file,
so no listing and no second scan come back.
- recordDelivered overwrote the delivered set on every recall, so a turn that hit
nothing erased the facts the previous turn was still holding and the next
recall had nothing left to re-check. It now keeps the previous set when this
recall returned no hits, while still appending the facts a write note carried.
Verification: a same-size/preserved-mtime rewrite is asserted first at the
Compose boundary (red on the stat-based check) and then at the true provider
boundary, through the real Build stack with a recording provider that shows the
refreshed body in the request the executor actually sends. The tracked path's
freedom from directory listing is asserted by arming the memory package's
read-dir seam to fail while stamping and while re-checking; FactAt is pinned to
the same single file. Both new tests go red with the stat-based conclusion
restored behind the same API and green with it removed.
TestRecallReloadsAFactEditedBeforeItsFirstRecall (seed, load, rewrite, Compose
twice) and TestRecallKeepsCheckingAfterATurnWithNoHits (recall A, an unrelated
no-hit turn, edit A, ask again) are red on the pre-fix behaviour and green now.
Each review fix was reverted alone with both tests kept: the first test goes red
and the second passes, and the reverse for the other. Go tests, vet, gofmt,
golangci-lint (0 issues) and make check (repolint: clean).
Refs #12948
An explicit expiry is a hard boundary: the fact index and automatic recall drop
the fact past it, and the memory tool paths report it as expired. Pinned guidance
did not check it, so an expired body kept riding the stable prefix, an expired
project fact kept shadowing the live global guidance it used to override, and an
expired body kept consuming the pinned budget that bounds what the prefix sends.
Both places now skip facts that are past their expiry, so the prefix, the shadow
rule and the budget agree with the index and recall. This covers facts that are
already expired when the session loads them; withdrawing a pinned body that
crosses its expiry mid-session is a separate prefix question.
An unqualified save whose name was active in two scopes was refused with a bare
sentence. A code caller could not tell that refusal from a write failure, and
the model received neither the two ids the name could have meant nor a way to
say which fact it wanted.
The refusal now wraps ErrNameActiveInTwoScopes, so errors.Is separates a
cross-scope name collision from a write failure, and its sentence renders that
code, both active ids with their scopes, and the two ways out: update the fact
you meant by its stable id, or save with a scope-qualified name. The remember
tool returns this error verbatim, and the agent renders it as the model-facing
tool result, so the identity and both ids reach the model without a second,
drifting sentence.
Tests cover the identity and both ids at the store and at the tool, the refusal
leaving both facts at their own bytes and revision, and the two unambiguous
saves (stable id, scope-qualified name) still succeeding. A neighbouring test
pins that a name active in exactly one scope is still updated, not refused.
An unqualified name that was active in another scope was archived after the save
was published, so a failure there left the same name active twice, and the
archiving itself removed a fact the caller never named.
The collision is now reported before anything is written, and the post-commit
archiving is gone.
git diff --numstat -z writes a rename or copy as the counts, a trailing
tab, then the old and new paths as their own NUL fields. Trimming that
record removed the tab, so the parser treated it as a short line and
dropped it. Status and Summary then reported no line counts for the
change. The counts stay on the new path, which is the name status lists.
git diff --numstat -z emits a rename or copy as the counts, a tab, NUL,
the old path, NUL, and the new path. Status and Summary need those counts
on the destination, beside an ordinary edit in the same diff.
The CLI/TUI shortcut tables in docs/GUIDE.md and docs/GUIDE.zh-CN.md still
described four 1.x behaviours the 2.x terminal UI does not have (#12144
lists them as missing):
- Up/Down "navigate queued follow-up feedback" while a turn runs; in 2.x
they recall history there too (recallAtEdge has no running case), and
queued items are managed with /queue.
- Esc "un-sends a just-submitted turn before any reply"; escape() cancels a
running turn, clears input, or leaves ! shell mode, and nothing un-sends.
- Composer text selection by drag with replace-on-type; onMouse ignores a
left click below the transcript viewport, so only transcript text is
selectable, and Ctrl+C copies only that.
- The rewind picker's f (fork); rewindActions has b c d s u.
Two tests pin the rows as now written: Up recalls while a turn runs, and
Esc on an empty composer leaves shell mode.
TestAuthorizeHTTPMCPDoesNotHoldStateLockDuringBrowser failed on Windows
CI (studio run 38416026313) with "removal during browser flow blocked on
OAuth state lock" after 1.09 s. The test runs ReconcileHTTPMCPOAuthAfterRemoval
from inside openURL and gave it one second. AuthorizeHTTPMCP holds no lock
there (captureMCPOAuthGeneration releases its lock before the browser
opens), so a slow removal on a loaded runner read as a held lock. Starting
the removal 1.2 s late reproduces the message.
The wait is now testenv.Budget(t). A lock really held across the browser
is still caught: the removal's own lock wait gives up after five seconds
and returns an error, which fails the test.
TestSaveSnapshotSucceedsWhenCrossProcessFileLockReleasesBeforeDeadline
failed on Windows CI (studio run 38502171680) with "lock session file:
session file lock held" after 1.28 s. A goroutine releases the competing
lock after 30 ms and the save waits at most 500 ms for it; on a loaded
runner the release came later than that. Releasing after 600 ms
reproduces the message.
The save returns as soon as the lock is free, so the wait now is
testenv.Budget(t). TestSaveSnapshotBoundsCrossProcessFileLockWait keeps
its 40 ms, since it asserts that a lock never released makes the save
fail.
TestHostContextCarriesTheExtraWritableFoldersToLocalServersOnly and
TestHostContextCapsHowManyWritableFoldersAreTold fail on every Windows run
since e6f70622b (studio run 38537039922): their roots are written as
"/data/shared" and "/data/000", which have no drive and so are not
absolute on Windows, and metaWritableRoots drops every root that is not.
The key is never sent and both tests see nil.
The roots are now built from the temp directory's volume, the repo's
usual way to spell an absolute path on any OS. The same change makes
TestHostContextSaysNothingOfWritableFoldersWhenThereAreNone exercise the
workspace exclusion on Windows: its "/w" was dropped as relative there,
so it passed whether or not the workspace was excluded.
Add a separately selectable naming model and adjustable reasoning preferences
for chat roles, with typed roles/efforts responses and user-only role settings.
Keep title generation fixed off with temperature zero and a 60-token budget;
reject explicit title effort instead of storing an unused setting. Use the
configured startup chat model when no naming model is selected, including
servers without a running controller.
Integrate the on-demand naming and rename paths from #11591 with grouped title
provider state. Preserve default-model persistence, per-profile model overrides,
busy-save notices, and inherited effort validation against the executing model.
Share the model-preference row's existing layout declarations to keep the CSS
line count below the studio baseline without changing rendered layout.
Preserve each provider's saved effort when a role has no stored preference;
use the model default only for explicitly selected auto. Add a real boot
assembly effect test for planner and guardian-derived reviewer/evaluator
providers, while leaving subagent effort inheritance unchanged.
Windows answers "not found" for a file where a directory should be, or
beneath one, so the not-exist shortcut hid the very case the directory read
error exists for. The stat of the entry and its parent tells them apart.
Problem: permission and storage failures can hide existing memory revisions,
make restores report them as missing, and compute recovery revisions from an
incomplete history.
Root cause: directory discovery conflated every ReadDir error with absence.
Fix: unify all eleven store walks behind a typed reader that treats only
missing directories as empty. Propagate errors through reads, save guards,
recovery, recall, controller commands, and HTTP responses. Complete recovery
and legacy duplicate discovery before writing. Preserve persisted formats.
Verification: failing-first permission fixture; eleven-site permission,
I/O, and non-directory regressions; unchanged files on failed recovery or
save; missing-directory compatibility; tool/controller/HTTP and bilingual
error coverage; Go tests, vet, lint, repository checks, frontend tests and
typecheck, and Windows memory test cross-compilation.
Fixes#12950
2026-10-14 11:23:56 +08:00
2210 changed files with 125775 additions and 7787 deletions
@@ -93,6 +93,16 @@ Prefer a terminal? Download the `reasonix` command-line tool, run `reasonix setu
All four are the same program underneath. What follows is for advanced users and contributors.
## Use it from DeepSeek Harness
DeepSeek Harness (`dsh`) can hand a task to Reasonix as a subagent over ACP. This repository publishes a plugin index for it:
1. Run `reasonix setup` on the machine that runs `dsh`. In the DSH Web UI turn on Custom plugin markets (Plugins, More) and add this index in the Discover tab: `https://raw.githubusercontent.com/esengine/DeepSeek-Reasonix/studio/integrations/dsh-plugin/market-index.json`
2. Install "Reasonix subagent" from it.
3. Open the plugin's page and turn its switch on. While it is off the model has no `reasonix` tool and may run `reasonix -p` in its own shell, which looks like a delegation and is not one.
Setup details, the permission policy and limits are in [Using Reasonix from DeepSeek Harness](./docs/DSH.md).
## Versions
Reasonix ships on two lines. See the [version roadmap announcement](https://github.com/esengine/DeepSeek-Reasonix/discussions/10748) for the reasoning behind the split.
@@ -240,7 +250,8 @@ For advanced CLI usage and configuration, see the **[CLI reference](./docs/CLI.m
1. Measured on 2026-10-17 against the official `https://api.deepseek.com` OpenAI adapter, using the model returned by `/models`: `deepseek-flash`. Thinking was disabled. Inputs were synthetic; no workspace history or credentials are recorded here.
2. Each of three text and three rich-history trials sent a main request to warm the prefix, then called the actual agent summary path, then an independent summary control. Rich history included a synthetic closed `read_file` call, result and reasoning. Warmup outputs were discarded, never executed.
3. A summary had a 1,024-token output cap; warmups had 64. Success means a heading, preservation of the fixture's `ParseDuration` and zero-value constraint, no emitted tool calls, and `finish_reason=stop`. This checks the fixture's contract, not arbitrary-summary factual completeness.
4. First-attempt reuse success counts successful reused summaries / six reused summaries. Fallback rate counts independent requests initiated by the reuse coordinator / six reuse trials; the six explicit independent controls are excluded. Both were 6/6 success and 0/6 fallbacks. Independent controls also succeeded 6/6.
5. The raw result file records provider-reported usage and full stream latency, measured from adapter `Stream` until channel close. Token rows below sum only the three indicated summary requests; they exclude warmup, model listing and any earlier exploratory requests. Cache residency was uncontrolled; prior probes had warmed these synthetic prefixes.
## Results
| History | Shape | N | Prompt | Hit | Miss | Output | Median ms | Estimated USD |
1. Estimates use the [official pricing](https://api-docs.deepseek.com/quick_start/pricing/) checked on the measurement date: off-peak Flash USD per million tokens, hit 0.003, miss 0.15, output 0.6. October 17 was a Saturday. Formula: `(hit*0.003 + miss*0.15 + output*0.6)/1_000_000`. These are token estimates, not account deductions.
2. Reuse hit fractions were 82.7% (text) and 81.3% (rich), versus 63.1% and 57.3% for controls. This small fixture does **not** establish cost savings: retaining the system/catalog increased both total input and input cost. Text total cost increased 6.8%; rich total decreased 12.5% because output was shorter, not because input was cheaper.
3. The six warmups consumed 7,032 prompt and 231 output tokens, estimated USD 0.000346680. All 18 recorded generation calls together cost an estimated USD 0.002491470. No failed-digest retry occurred in these live trials; the deterministic fallback tests establish its behavior and accounting.
4. A rejected nonempty reused answer earns one independent-shape fallback. A separate truncation retry may enlarge the output cap, for at most three logical model calls total. Every actual attempt emits and records its own spend; returned usage is their sum. Empty output, stream errors and cancellation do not earn shape retries.
5. These sequential, six-pair trials establish interoperability for one model, mode and short fixture. They do not estimate production failure probability, thinking-mode behavior, other models, cold-cache cost, or large-history savings.
## Reproduction
1. Provide a test key through an existing secret environment configuration. Do not put credentials in source, command arguments or captured logs. Select a model currently returned by `/models`.
2. Run the opt-in probe; it skips without `DEEPSEEK_API_KEY`. The dedicated build tag avoids enabling unrelated live tests.
```sh
REASONIX_LIVE_MODEL=deepseek-flash go test -tags live_compaction ./internal/runtime/agent -run '^TestLiveCompactionPrefix$' -v -count=1
```
3. This probe exercises actual summary construction and the HTTP adapter. The hermetic `TestEffectCompactionSharesThePreviouslySentPrefix` separately exercises manual, automatic pressure and rich history through `boot.Build`, comparing actual JSON request-prefix and schema bytes. Its sparse-history case also proves an interleaved retained user turn survives the fold.
// The split counts are absent; the checkpoint-derived ones are not.
line+=fmt.Sprintf("\n\n**Correct boundary**: **mutations before** %d · **regression-after-correct** %s — nothing to average: the runs reached a correct state, but none recorded a trajectory, so the boundary was never split",
constback=awaituntil("the page beside the conversation again",async()=>(!beneath()&&width()<placed.width?guest.getBounds():null));
// The column settles a pixel or two after the first measurement, so beside means
// the same place and size to within a few pixels.
check("leaving the full-width view puts the page back beside the conversation",Math.abs(back.width-aside.width)<=3&&Math.abs(back.x-aside.x)<=3&&!beneath(),{back,aside});
check(`${name}: the view is on screen, beside the conversation`,b.x>=0&&b.width>100&&b.height>100,b);
check(`${name}: the title is the image's own`,guest.webContents.getTitle().includes(path.basename(file)),guest.webContents.getTitle());
check(`${name}: what the view drew is the picture, not a blank`,share>0.2,{share,size,bounds:b,scale:require("electron").screen.getPrimaryDisplay().scaleFactor});
check(`${scheme}: icon ${i+1} of the message's own row matches the reply's`,awaitsize(mine.nth(i))===replyIcon,`${awaitsize(mine.nth(i))} vs ${replyIcon}`);
}
check(`${scheme}: the buttons are the same size`,awaitsize(page.locator(".user-acts > button").first())===replyButton);
check(`${turns} turns: the current branch is selected`,awaitpalette.getByRole("option",{name:"main",exact:true}).getAttribute("aria-selected")==="true");
check(`${tag}: the badge is on the wordmark's row, vertically centred on it`,m.shown&&m.dy<=1.5&&m.after,JSON.stringify(m));
check(`${tag}: it is an outlined uppercase badge in both themes and the text is untouched`,m.fill==="rgba(0, 0, 0, 0)"&&m.border!=="0px"&&m.radius!=="0px"&&m.upper==="uppercase"&&m.text==="studio",JSON.stringify(m));
if(!seen)seen={left:m.left,top:m.top};
check(`${tag}: it sits in the same place whichever theme is on`,Math.abs(m.left-seen.left)<=0.5&&Math.abs(m.top-seen.top)<=0.5,`${m.left},${m.top} vs ${seen.left},${seen.top}`);
}else{
check(`${tag}: the badge stays hidden on a narrow window`,!m.shown);
check(`${scheme}: no icon column or vertical track`,m.iconColumn==="none"||m.iconColumn==="absent",m.iconColumn);
check(`${scheme}: no box around the row`,m.sides==="0px,0px,0px"&&m.radius==="0px"&&(m.bg==="rgba(0, 0, 0, 0)"||m.bg==="transparent"),JSON.stringify(m));
check(`${scheme}: opened, with its caption above`,m.open&&m.caption);
f({kind:"turn_done",receipt:{verdict:"partial",changes:[{path:"a.go",reviewed:true}],verifications:[{command:"go test ./...",passed:false,stale:true}],gaps:[{kind:"unverified_mutation",detail:"改了没跑"}],risks:[],unverified:[],saysSomething:true}});
check(`${scheme}: an opened step has a detail panel`,m.length>0,`${m.length} found`);
check(`${scheme}: it is bordered on all four sides`,m.length>0&&m.every((x)=>x.sides==="1px,1px,1px,1px"&&newSet(x.color).size===1),JSON.stringify(m[0]??{}));
@@ -40,26 +40,24 @@ for (const scheme of ["light", "dark"]) {
check(`${tag}${round}: the browser column is drawn`,!!open.side);
if(open.side){
if(label==="wide"){
check(`${tag}${round}: it starts at the top of the pane, beside the navigation row`,Math.abs(open.side.top-open.pane.top)<=1&&Math.abs(open.tabs.top-open.pane.top)<=1,`pane ${open.pane.top} tabs ${open.tabs.top} column ${open.side.top}`);
check(`${tag}${round}: it starts at the top of the pane`,Math.abs(open.side.top-open.pane.top)<=1,`pane ${open.pane.top} column ${open.side.top}`);
check(`${tag}${round}: it runs to the bottom of the pane`,Math.abs(open.side.bottom-open.pane.bottom)<=1,`${open.side.bottom} against ${open.pane.bottom}`);
check(`${tag}${round}: the navigation row stays left of it`,open.tabs.right<=open.side.left+1,`${open.tabs.right} / ${open.side.left}`);
check(`${tag}${round}: its strip ends on the navigation row's line, so the two rows read as one`,!!open.strip&&Math.abs(open.strip.bottom-open.tabs.bottom)<=1,JSON.stringify([open.tabs,open.strip]));
check(`${tag}${round}: the workbench card's own edge separates it from the conversation`,open.divider>=1,String(open.divider));
}else{
check(`${tag}${round}: the navigation row stays above the page`,!!open.tabs&&open.side.top>=open.tabs.bottom-1,JSON.stringify([open.tabs,open.side]));
check(`${tag}${round}: the page takes the pane's full width when it stacks`,open.side.left<=open.pane.left+1&&open.side.right>=open.pane.right-1,JSON.stringify([open.pane,open.side]));
check(`${tag} undocked: the column is gone and the navigation row is as wide as the pane's body again`,(!shut.side||shut.side.left>=shut.pane.right-1)&&!!shut.tabs&&shut.tabs.right-shut.tabs.left>=shut.pane.right-shut.pane.left-24,JSON.stringify([shut.side,shut.tabs,shut.pane]));
check(`${tag} undocked: the column is gone`,!shut.side||shut.side.left>=shut.pane.right-1,JSON.stringify([shut.side,shut.pane]));
awaitpage.close();
}
}
// Every room between phone and wide, not two samples: the left column shrinks with the window, and the tabs it holds
// must still fit and stay clear of the browser column.
// Every room between phone and wide, not two samples: the conversation shrinks with the window, and it must
// stay clear of the browser column, which stacks over it once the window is narrow.
check("from 560 to 1440 the tabs fit their row, stay clear of the browser column, and the column stacks exactly when tabs plus its 320px floor no longer fit",bad.length===0,bad.join(" "));
check("from 560 to 1440 the conversation stays clear of the browser column, which stacks only in a narrow window",bad.length===0,bad.join(" "));
check(`${sc.name}: every piece of text answers to the text size or the reading size`,holes.size===0,`${total} text nodes${holes.size?"; stuck: "+[...holes.keys()].slice(0,6).join(", "):""}`);
constcontent="# Release plan\n\nThe plan lives in the library folder you granted this project.\n\n- freeze on Friday\n- tag after the checks are green\n- announce the next morning\n";
constcontent="# Install guide\n\nDownload the package for your system, then run the installer. It places the program in your user folder and does not need administrator rights.\n\nAfter the first start, open Settings and add a model service before you begin a conversation.";
check(`${tag}: the tip has the name and the full command, inside the window`,tip.inside&&tip.lines===2&&/npm run tauri:test/.test(tip.cmd),JSON.stringify(tip));
check(`${tag}: its text can be selected`,tip.select==="text",tip.select);
awaitpage.mouse.move(width-5,5);
awaitpage.waitForTimeout(200);
check(`${tag}: it closes when the pointer leaves`,(awaitpage.locator(".job-tip").count())===0);
check(`${tag}: a 401 row offers sign-in beside the retry`,r.figma?.includes("登录")&&r.figma.includes("重连"),r.figma?.join("|"));
check(`${tag}: a row with withheld tools offers both approvals`,r.notes?.includes("批准并重新连接")&&r.notes.includes("接受并以后都自动接受这个服务器的改动"),r.notes?.join("|"));
check(`${tag}: a followed row says so and offers to stop`,r.followNote&&r.wiki?.includes("取消跟随"),r.wiki?.join("|"));
check(`${tag}: no button spills out of its row`,r.spill.every((n)=>n===0),r.spill.join(","));
check(`${tag}: no two buttons overlap`,r.overlap.every((o)=>!o));
check(`${tag}: the focus ring of the band's button is not cut off by the strip it sits in`,ring.room>=ring.ring-0.5,`${ring.room}px room for a ${ring.ring}px ring`);
check(`${tag}: a mod's question says whose it is and that answering approves nothing`,card.note.includes("git-guard")&&card.note.includes("不会批准任何工具调用或权限")&&card.tag.includes("git-guard"),card.note);
check(`${tag}: the switch ships off and every mod says why it is not running`,off.checked==="false"&&off.rows===4&&off.why===4,`${off.rows} mods, ${off.why} reasons`);
check(`${tag}: each state shows running, disabled, restarting or not running, with a mark as well as a word`,on.states.join()==="running,disabled,restarting,inactive"&&on.marks,on.states.join());
check(`${tag}: the block does not spill sideways`,!on.spill);
awaitpage.locator('textarea[placeholder*="github.com"], input[placeholder*="github.com"]').first().fill("https://github.com/acme/claude-code mod git-guard");
check(`${tag}: the install preview states full trust, that it is a static listing, hidden imports and unserved events`,prev.trust&&prev.static&&prev.imports&&prev.unserved&&prev.hash,JSON.stringify(prev));
check(`${scheme}: three notices drawn as bars`,m.length===3,`${m.length} found`);
check(`${scheme}: no box, wash or header strip`,m.length>0&&m.every((x)=>x.border==="0px,0px,0px,0px"&&(x.bg==="rgba(0, 0, 0, 0)"||x.bg==="transparent")&&!x.header),JSON.stringify(m[0]??{}));
check(`${scheme}: severity is the glyph ink alone`,m.length===3&&newSet(m.map((x)=>x.glyph)).size===3,m.map((x)=>x.glyph).join(" / "));
check(`${scheme}: four sessions carry a mark, the rest none`,marked.length===4&&marked.map((r)=>r.state).sort().join()==="declined,replied,waiting,working",marked.map((r)=>r.state).join(","));
check(`${scheme}: each mark says its state in words`,marked.every((r)=>r.label&&r.label.length>6));
check(`${scheme}: each mark sits inside its row`,marked.every((r)=>r.inside));
check(`${scheme}: no mark covers the title`,marked.every((r)=>r.clear));
check(`${scheme}: marks are small`,marked.every((r)=>r.size>=14&&r.size<=22),marked.map((r)=>r.size).join(","));
check(`${tag}: the switch is on the page`,s.enabled==="true");
check(`${tag}: the hand-off switch is live while messages are on`,s.dispatchOff===false);
check(`${tag}: released projects are listed with a revoke each`,s.released.length===2&&s.revoke.length===2&&s.revoke.every((l)=>l.startsWith("撤销放行")),s.revoke.join("|"));
check(`${tag}: a saved switch says the next turn, never at once`,s.pending&&!s.immediate);
check(`${tag}: the file the switches live in is named`,s.path);
check(`${tag}: nothing spills out of the box`,s.within&&s.spill<=1,`${s.spill}px`);
check(`${scheme}: the search box stays open after focus leaves it, because it holds a query`,held.shown);
check(`${scheme}: pressing a result does not move the list under the pointer`,Math.abs(moved)<1,`${moved}px`);
check(`${scheme}: the first press on a result becomes a click on that result`,events.some((e)=>e.startsWith("click ")&&e.endsWith("[session.open-match]")),events.join(" | "));
awaitpage.getByRole("searchbox").fill("");
awaitpage.keyboard.press("Tab");
awaitpage.mouse.click(700,500);
awaitpage.waitForTimeout(250);
constidle=awaitstate();
check(`${scheme}: with no query the box still folds away when it loses focus`,!idle.shown);
check(`${tag}: it lists the other projects but not the one it is in`,dlg.options.length===2&&!dlg.options.some((o)=>o.startsWith("workspace-0")),dlg.options.join(" | "));
check(`${tag}: it says what travels and what does not`,dlg.text.includes("原项目的权限与目标设置不会跟着走")&&dlg.text.includes("路径保持原样"));
check(`${tag}: the dialog fits the window and holds focus`,dlg.fits&&dlg.focusIn,JSON.stringify({fits:dlg.fits,focusIn:dlg.focusIn}));
check(`${tag}: the kernel is asked once, for this conversation and the chosen project`,moves.length===1&&moves[0].path===first&&moves[0].target.endsWith("workspace-2"),JSON.stringify(moves));
check(`${tag}: # lists past conversations marked as conversations`,menu.rows>=2&&menu.kinds.length===1&&menu.kinds[0]==="会话",`${menu.rows} rows ${menu.kinds}`);
check(`${tag}: each row says when it was`,menu.hints);
check(`${tag}: the menu stays on screen`,!menu.spill);
check(`${tag}: picking one makes a chip titled with it and takes the # word out of the box`,!!chip.title&&chip.text==="接着上次的讨论 "&&chip.sub==="历史会话 · 只读资料",`${chip.title} | "${chip.text}"`);
check(`${tag}: the chip can be removed and does not spill`,chip.remove&&!chip.spill);
check(`${tag}: a search hit offers to reference it, beside the open button`,hit.has&&hit.beside&&/引用「.+」到输入框/.test(hit.label),`${hit.size}${hit.label}`);
check(`${tag}: that button is at least 28px tall`,hit.tall,hit.size);
check(`${tag}: the size limit is a number field at its default, with nothing to restore`,s.value==="8000"&&s.unit==="token"&&s.restore===true,`${s.value}${s.unit}`);
check(`${tag}: the setting does not spill sideways`,!s.spill);
check(`${tag}: the menu retains Pin and Archive`,awaitpage.getByRole("menuitem",{name:"置顶会话",exact:true}).count()===1&&awaitpage.getByRole("menuitem",{name:"归档会话",exact:true}).count()===1);
check(`${tag}: Delete stays in the menu`,awaitpage.getByRole("menuitem",{name:"删除会话",exact:true}).count()===1);
check(`${label}: it sits above the composer`,p.bottom<=r.compose.top+1&&r.compose.top-p.bottom<24,`bottom ${Math.round(p.bottom)}, composer top ${Math.round(r.compose.top)}`);
check(`${label}: it stays inside its pane`,p.left>=r.pane.left-1&&p.right<=r.pane.right+1&&p.top>=r.pane.top-1,`panel ${Math.round(p.left)}–${Math.round(p.right)}, pane ${Math.round(r.pane.left)}–${Math.round(r.pane.right)}`);
check(`${label}: it stays inside the window`,p.left>=0&&p.right<=r.vw&&p.top>=0&&p.bottom<=r.vh);
check(`${label}: it does not cover the file panel`,r.beside.every((b)=>p.right<=b.left||p.left>=b.right||p.bottom<=b.top||p.top>=b.bottom),files?`${r.beside.length} file panel parts`:"no file panel");
check(`${label}: it opens above the button that asked`,p.bottom<=r.button.top&&p.left<=r.button.left+1,`button ${Math.round(r.button.left)}`);
check(`${label}: a running session's dot ${reduced==="reduce"?"does not move":"breathes"}`,reduced==="reduce"?names.running==="none":names.running==="breathe",names.running);
check(`${label}: idle, halted and finished dots stay still`,names.idle==="none"&&names.halt==="none"&&names.done==="none",JSON.stringify(names));
check(`${label}: an unread finished session is told apart by its own dot, not an extra one`,names.unreadDiffers===true);
check(`${tag}: the control reaches the page`,scale===1?m.scale==="":Number(m.scale)===scale,`--fs-scale "${m.scale}"`);
check(`${tag}: type grows by the step`,Math.abs(m.sessionTitle/base.sessionTitle-scale)<0.03&&Math.abs(m.newTask/base.newTask-scale)<0.03,`${base.sessionTitle}→${m.sessionTitle}, ${base.newTask}→${m.newTask}`);
check(`${tag}: the layout keeps its room`,m.railWidth===base.railWidth&&m.composeWidth===base.composeWidth&&m.zoom===base.zoom,`rail ${m.railWidth}/${base.railWidth}, composer ${m.composeWidth}/${base.composeWidth}`);
check(`${tag}: every picture stays inside a 240x160 thumbnail`,rows.imgs.length===4&&rows.imgs.every(({img})=>img.w<=241&&img.h<=161),JSON.stringify(rows.imgs.map((x)=>[Math.round(x.img.w),Math.round(x.img.h)])));
check(`${tag}: no picture leaves its own box`,rows.imgs.every(({box,img})=>img.t>=box.t-1&&img.b<=box.b+1&&img.l>=box.l-1&&img.r<=box.r+1));
check(`${tag}: the message text is not covered by a picture`,rows.text!==null&&rows.imgs.every(({img})=>img.b<=rows.text.t+1||img.t>=rows.text.b-1||img.r<=rows.text.l+1||img.l>=rows.text.r-1));
awaitpage.close();
}
}
awaitbrowser.close();
if(fails.length){
console.log(`\n${fails.length} failed`);
process.exit(1);
}
console.log("\nall ok");
Loaded 100 of 2210 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.