Problem: plugin and theme packages could be published with a mistyped
41-character commit reference and fail when clients tried to install them.
Root cause: registry validation accepted GitHub repository paths without
requiring the commit-pinned source contract enforced by the installer.
Fix: require HTTPS GitHub tree URLs with full 40-character lowercase
hexadecimal commits, retaining the existing path safety checks. Update
publication fixtures and cover direct HTTP submissions before persistence.
Verification: all 801 Worker tests, TypeScript checking, Wrangler deployment
dry-run, and git diff --check passed on the current platform baseline.
A reply to a fixed report moves it back to recorded and clears
resolved_version in the same batch as the reply insert, so the reply
reaches the converter and a later fix can be recorded again. wontfix and
duplicate stay non-replyable.
Objects carry a one-hour cache lifetime and, when a zone and Cache Purge token
are configured, are purged from the edge on removal. A removal never touches a
pin another live package uses or a kept version shares. The package is re-read
before and after the write so a hide that lands mid-fetch cannot be undone.
Fetches time out, the size cap matches the installer's 2 MiB, retention counts
only versions that have an object and walks every version page, and every key is
asserted to sit under market/skills/.
After a skill is approved or pinned, the worker fetches its SKILL.md from the
GitHub repository, requires an OSI licence, requires the bytes to reproduce the
reviewed pin (the installer's digest over the skill name and file hash), and
stores them under market/skills/<pin>/SKILL.md on the existing download bucket.
A hide or reject removes every mirrored version; only the two newest versions
are kept. Off by default (MARKET_MIRROR_ENABLED).
The converter decides whether a fixed entry is still waiting for a tag from
resolvedVersion, but the open listing dropped the column, so no fixed(next)
entry was ever upgraded to a concrete version and nothing was credited.
Adds feedback_account_links, link/unlink/list routes, a union read for linked
installs and sibling replies, all behind FEEDBACK_ACCOUNT_LINK (off by default),
plus a signed erase endpoint for the accounts worker.
Adds POST /me/feedback-assertion (short-lived, audience-scoped) and tells the
feedback worker when an account is deleted, through a retried outbox that never
blocks the deletion.
EventRepo.recent kept every event whose package was not private, so the
public feed went on listing a package after an admin hid it, after a
publisher's update sent it back to review, and after that update was
rejected. Each of those packages answers 404 everywhere else, and the feed
is cacheable since #12207, so the stale entries also outlive the takedown.
The feed now keeps an event only when its package is active, the same rule
the listing and detail routes apply; events with no package are kept.
Fixes#12208
List, search, detail and activity now send a 60 s public Cache-Control and a
weak ETag, answer If-None-Match with 304, and are stored in the edge cache.
Credentialed requests, errors and every other route are no-store.
Add the platform-side enrollment record for phone and browser controllers
described in RFC #11878 section 10, stage 1. It is bookkeeping only: no
grant, gateway or host path reads it yet, so it adds no protection to
remote access.
- migration 0007: remote_controllers, remote_controller_challenges and
remote_rate_counters (additive, idempotent; rows are never deleted)
- routes: challenge, enroll, list and revoke under /me/remote-controllers;
pending list, resolve and revoke under /host/controllers, authenticated
with the computer's own device credential
- the enrollment id is derived from host id and key thumbprint; the server
recomputes the thumbprint from a strictly validated JWK and verifies an
ECDSA P-256 proof of possession over a labelled, length-prefixed message
- nonces are single use, hashed, bound to user, host, session and purpose,
and consumed before the signature is verified
- `revoked` is terminal: re-enrolling a revoked key is refused and no code
path clears revoked_at; ordinals are max+1 per host and never reused
- the first device on a computer auto-enrolls; any further key is held as
pending until the computer resolves it; at most 3 pending and 8 recently
used active devices per computer, with replace-least-recently-used
- limits are D1 counters that refuse the request when the counter cannot be
written; a password change, reset or account deletion revokes every
enrollment in the same batch as the host registrations
Credit an install when a maintainer moves its report to fixed with a concrete
published version, and derive quotas from one resolveFeedbackLevel policy
instead of the flat trusted flag.
- feedback_adoptions ledger (unique per install+item and per receipt, tombstones),
feedback_level_state (high-water count, revocation marker), additive idempotent
migration applied and verified by the manual platform deploy before the Worker.
- The status transition, credit, trust renewal, high-water mark and audit row
commit in one batch; replays, retries, duplicates and corrections never
double-credit; tombstoned items cannot be credited again.
- submit/reply admission and the read-only profile on GET /v1/feedback/mine
consume the same resolution; L0 is the untrusted tier and L6 the shipped
12/60/10 tier, trust without credit stays legacy_active.
- Admin POST/DELETE /v1/admin/feedback/<receipt>/adoption record and tombstone.
The maintainer's network resolves *.workers.dev to a proxy fake-IP and the TLS handshake fails, so the local listener could not connect. Attach a custom domain in our own zone, same mechanism as id.reasonix.io; workers.dev stays enabled.
Cause: when R2 is unavailable, the stable desktop manifest gateway asked
GitHub for the repository-wide latest release and accepted it only if its
tag was desktop-v*. Studio is about to become that latest release, so the
fallback would answer 502 instead of the newest 1.x desktop manifest.
Blast radius: only the stable desktop fallback in desktop_release.ts,
reached when the R2 pointer is missing or invalid. The R2-first path,
preview/canary handling, the CLI fallback and the manifest validation
(tag/version match, canonical latest.json asset URL, size bounds) are
unchanged. The 502 body gains a reason field. Not deployed.
Neighbouring behaviours tested: vitest (467 tests) and tsc --noEmit pass.
Cases: newest desktop-v* chosen among studio-v*, v*, draft, prerelease,
semver-prerelease, leading-zero and asset-less entries; numeric ordering;
desktop release on page 3 behind 200 other releases; page cap reached gives
"no-desktop-release-found"; a failing later page gives "upstream-error".
Why this layer: the gateway owns the fallback decision. It pages the
listing (100 per page, at most 10 pages) until a page holds a valid
desktop-vX.Y.Z release with a conforming latest.json, and picks the highest
strict semver on that page, because Studio and CLI releases are frequent
enough to push desktop releases off a single page.
An answered, held or needs_info report can never reach 'recorded': the
converter endpoint only takes 'received' and the status machine is
forward-only. Add POST /v1/admin/feedback/:receipt/link (admin only) that
takes only an issue number, derives the URL from the repository, and moves
held/needs_info/answered/received to recorded. The same issue again is a
no-op; a different one is feedback.issue_conflict.
Answering dropped the report's images, so the admin page could never show
the screenshot of an answered item. Keep them: they stay unreleased
(release only accepts held/received), admin-only, and the 30-day retention
sweep already covers answered rows. Reject and takedown still delete them.
Submit, user reply and admin status changes now post a best-effort event
(fixed label, receipt, category, status; no user text) to the ops-events
relay through ctx.waitUntil. Unset relay env is a no-op; a failing or
slow relay cannot change the HTTP response.
A Durable Object keeps an ordered, bounded event log and fans events out to at most four WebSocket listeners with hibernation, so an idle listener costs nothing and nothing polls a database. GitHub deliveries are verified by HMAC before parsing and deduplicated; only compact metadata (type, number, a 120 character title, author, link) is ever forwarded, never bodies. Our own workers can push through a separate emit token with a rate limit.
Adds GET /v1/admin/feedback/list (filters, cursor paging, newest first) and a
self-contained page at /admin/feedback with a detail view and the existing
triage actions. The token lives in sessionStorage and the Authorization header
only; user text is rendered with textContent under a strict CSP. Wrong admin
tokens now lock the caller out of the whole admin API for the window.
Submissions are held by default and auto-released only for trusted installs
without images. Adds triage, reply, block, takedown and cap endpoints, private
attachments until released, optional Turnstile, and an idempotent migration
wired into the deploy workflow.
Refs #11424
A browser cannot read why a WebSocket upgrade failed, so a 401, 429 or 403
from the relay reaches users as one generic sentence and left nothing to
trace afterwards. Each refusal (origin, credential, rate limit, expired
grant, sign-in age, revoked or full room) and each lease or socket close
(idle, sign-in age, revoked, re-check failure) now logs code, reason, role,
age and idle time, plus a 32-bit hash of the device or grant. Tokens and
full ids are never logged. Each event+code is capped at 20 lines a minute
per isolate and the next window reports how many were skipped.
Worker side of the feedback loop: D1 feedback and quota tables, issue-once install tokens, metadata-free image attachments in TELEMETRY_RAW under feedback/, layered abuse limits, retention in the daily purge, kill switch, and the converter admin endpoints. Refs #11424
- Remote grants and lease checks accept only password (web) sessions: a
device-flow session is minted by approving from any session, so its age
could reset the 24 hour sign-in limit indefinitely.
- The gateway refuses a grant without reauthAt instead of assuming 24
hours from now, and a controller lease without one does not parse.
- Admission time is taken before the account service answers, and the
room remembers session sign-outs for ten minutes, so an admission in
flight during a revocation is refused.
- Failed re-checks fail open for at most an hour; then every connection
must be admitted again.
- A full room closes its quietest controller (silent for a minute) rather
than refusing a reconnecting phone for half an hour.
- Revocation pushes retry once.
The relay checked a never-renewed 15 minute deadline on every controller
message, so an active phone was cut off mid-use. A credential is now
checked only at the WebSocket upgrade. After that each connection holds a
lease that slides with its own frames (30 minutes idle), a device lease
also slides with a heartbeat the runtime answers without waking the
object, and a controller ends when the account session that admitted it
is 24 hours old.
The age limit is enforced where it cannot be reset by reconnecting: the
account service refuses to issue a grant from a session older than 24
hours (remote_reauth_required), and every grant now records the session
that asked for it. The relay re-checks each live connection against the
account service every five minutes, and removing a device, changing or
resetting the password, deleting the account or signing out pushes an
immediate disconnect.
Close codes carry the cause: 4408 idle (reconnect silently), 4401 sign in
again, 4403 revoked. Controllers stay attached while their device
reconnects, and the device is greeted with the list of attached
controllers so it can keep their encrypted sessions.
Refs #11226
Desktop builds up to v1.38.3 reject the electron-v1 install layout that every
current manifest carries, so their update check fails with a retryable-looking
error and the banner offers "retry" forever.
The release gateway now reads the updater's User-Agent version. For a strict
Reasonix-Updater/vX.Y.Z at or below v1.38.3 it answers with a manifest whose
version field is a short bilingual instruction ending in "manual update
required". Those updaters validate the version first and quote it verbatim
into the check error, and their frontend classifies any error containing that
phrase as manual, which turns "download from reasonix.io" into the primary
action. Everything else (newer builds, missing or malformed UA) is forwarded
unchanged.
The guidance response is private, no-store; every desktop manifest response
now carries Vary: User-Agent so no shared cache can hand one population the
other's answer.
The 1.x desktop renders with Chromium (Electron) and sends
runtimeEngine "chromium" on /v1/ping and /v1/metrics. Both schemas only
allowed webview2, webkitgtk and unknown, so every ping and metrics batch
from a current desktop install failed validation with 400 and never
reached storage: desktop daily actives and metrics covered only installs
still on the older shells.
Add chromium to the shared enum and to the stats engine filter.
* feat(registry): votes, deduped install counts and a recommended order
One account, one vote per package (+1, -1 or none), changeable and
withdrawable; only verified accounts vote and a publisher never votes on
their own package. The vote and a recount of packages.up_count/down_count
from the votes table run in one D1 batch, so the cached counts cannot
drift. Legacy stars migrate to +1 votes (self-stars excluded, stars kept);
the star endpoint now toggles a +1 vote.
Install pings count once per (anonymous install id, package, UTC day):
a seen row is inserted fresh, the counters move only while it is fresh,
and the batch spends it before commit. Pings without an id are
acknowledged and not counted. No IP is stored; dedupe rows are purged
once their day has passed.
sort=recommended becomes the default: 0.6 x Wilson 95% lower bound of
the vote ratio + 0.4 x ln(1 + 30-day installs decayed with a 14-day
half-life), normalized against a fixed cap. The score is materialized in
packages.rec_score (per package on vote/counted install, all packages by
cron, on demand via POST /v1/admin/rescore) so the listing still sorts
and paginates in SQL. Packages with >= 10 down-votes under 30% approval
are listed as Flagged in the moderation console; nothing is auto-hidden.
The formula is published in REGISTRY-RANKING.md and lib/ranking.ts.
* feat(registry): weight heat below reputation and count distinct install ids
Anonymous install ids can be minted freely, bounded only by the per-IP
write limit, so heat now carries 0.25 of the recommended score (0.75
reputation) against a cap of ln(1 + 5000), and counts each distinct id
of the 30-day window once, decayed from its latest install. However many
ids are minted, heat cannot lift a package above the reputation alone of
one with a solid vote record. Dedupe rows are kept for the window instead
of two days; still no IP is stored.
* feat(registry): keep private packages out of votes, installs and ranking
A private package already answers 404 to votes, stars and install pings
because each requires an active package; the cron rescore now skips it
too, so it never carries a recommended score. Tests pin all four paths
and that the listing serves pinned alongside the vote fields.
A publish may now ask for `visibility: "private"`. Such a package lands in
the `private` status: outside the review queue, and absent from every public
surface (listing, search, detail, activity feed, install and star pings).
- GET /v1/me/packages/:handle/:name returns the caller's own package in any
state with its versions, so a publisher can install an unreviewed or private
package of their own. Another account's package answers exactly like a
missing one.
- POST /v1/me/packages/:handle/:name/submit moves an owner's private package
to pending; nothing else can put it in front of a reviewer. Approval
refuses a private revision, and moderators cannot hide, reject or verify
one into a state approval would accept.
- Each publish follows its own visibility: a public update of a private
package is a submission, a private update of a live one takes it down.
- The moderation console gains a read-only Private tab.
No schema change: status is free text, so no migration is needed.
The digest input added for pinning sat inline beside the other actions,
and the full 71-character digest was printed in the actions column, so the
column overflowed the card and the inputs rendered unstyled. The current
digest now shows abbreviated under the package name (full value on hover),
pinning is a collapsed control that opens a styled field, and the approve
field sits on its own line above Approve and Reject.
Packages approved before the reviewer could record a content digest are
listed in the Studio market but refused there as unpinned, and nothing let
an admin bind one after the fact.
- POST /v1/admin/packages/:handle/:name/pin (requireAdmin) and a Pin/Re-pin
form on the /community console's Active tab bind a sha256:<64 lowercase
hex> digest to the live package's current version.
- The write is fenced like approval (latest_version, updated_at, and status
must be the active revision the admin saw) and is compare-and-set on the
digest it read, so a concurrent publish or re-pin answers 409 instead of
being overwritten. Package status and updated_at are untouched.
- Every pin logs a registry event (type "pin", actor, time, "<version>
<old|unpinned> -> <new>"); the console also writes its audit_log entry.
Pin events stay out of the public activity feed.
- The console's review list now shows each package's current digest.
The listing now carries a read-only `pinned` per package: whether its latest
version holds a reviewer-bound sha256 content digest, the same shape the
publish schema accepts and the client installs against. `pinned=1` filters in
the query itself, so limit/offset pages stay correct instead of a client
dropping rows from a page it already received.
The check is an indexed EXISTS against package_versions(package_id, version);
no schema change or migration.
- kind gains `theme`: its own listing category, installed through the
plugin installer. The stored and served installKind is `plugin`, and a
theme source is held to exactly the plugin source rules (a GitHub
repository or repository path), so a theme cannot reach the generic URL
or npm fallbacks the plugin kind already refuses.
- GET /v1/me/packages (requireAuth) lists every package the caller
published, in every review state (pending, active, rejected, hidden), so
a submitter can see where review stands. The public listing still serves
active rows only.
- The worker entry routes /v1/me/ to the registry app; the trailing slash
keeps /v1/metrics out of it.
No schema migration: kind is unconstrained TEXT.
- The identity lookup never follows a redirect, and an accounts outage
answers 503 accounts_unavailable instead of 401, so a signed-in
publisher is not told to sign in because id.reasonix.io was down.
Studio's community market installs a listed package only when its approved
version carries a content digest, and install_source refuses material that
no longer matches it. That pin has to come from the review, not from the
publisher: a publisher who supplies the digest of content they later swap
into a mutable source would otherwise have their claim promoted to a pin.
- Approval (JSON admin API and the /community console) takes an optional
contentHash - the contentDigest Reasonix prints for the plan the reviewer
read - and writes it onto the reviewed version row before the package goes
public, under the same revision fence as the status change. Approving
without one clears any publisher-supplied value: the version is published
but not installable from Studio.
- contentHash, on publish and on approval, must be sha256:<64 lowercase hex>
(install_source's spelling) or empty.
Adds /me/backups (list, upload, download, delete) for the Studio cloud
backup. The client seals each backup with a passphrase before upload, so
the service keeps an opaque envelope in R2 (backups/<user>/<id>) and only
listing metadata in D1 (config_backups). Every query and object key is
scoped by user id; an account keeps at most 10 backups of up to 4 MiB,
enforced in the same INSERT so concurrent uploads cannot overshoot.
Deleting an account removes its backups.
Deploy needs the reasonix-config-backups R2 bucket bound as BACKUPS and
migration 0005; without the binding the routes answer 503.
The embedded Pi catalog (sky-valley/pi v0.84.20, unchanged in v0.87.24)
annotates minimax-m2.7 as openai-completions. Both the Endpoints table on
opencode.ai/docs/go and models.dev (the catalog the OpenCode client reads)
serve it on /v1/messages with @ai-sdk/anthropic. Because the route
catalogs are the union of Pi and the declared tables, minimax-m2.7 was
listed on the Chat Completions route too: the curated "opencode-go" chat
preset shipped it, discovery kept it for a chat connection, and
OpenCodeGoRecommendedRoute (Pi first) answered "chat".
A declared correction table in pi_catalog.go now pins minimax-m2.7 to the
Anthropic route, and Pi entries are dropped from any other route. The Qwen
models keep their current dual-route state: models.dev marks
qwen3.6-plus, qwen3.7-max, qwen3.7-plus and qwen3.8-max
@ai-sdk/openai-compatible, so the chat route is not shown to be wrong.
Only connections that are ours move. The load-time route repair and the
journaled pre-v10 upgrade both leave a route-corrected model where it is,
unless the connection has preset_id "opencode-go" on the official chat
route and its models equal a chat list v1.37.0-v1.39.1 shipped (two
declared generations). Such a connection gets an opencode-go-anthropic
sibling for minimax-m2.7, and references to it are retargeted. Hand-made
connections, including config_version>=10 manual choices, are untouched.
Test: on the merge base, TestOpenCodeGoMiniMaxM27ServedOnlyOnAnthropicRoute
(recommended route "chat") and
TestOpenCodeGoShippedChatPresetMovesMiniMaxM27ToAnthropicSibling (no
anthropic sibling) fail. Both pass after the change.
TestOpenCodeGoManualConnectionKeepsItsRoute covers a manual
"custom-opencode-ai" connection with [glm-5.3 qwen3.7-plus],
[qwen3.7-plus], [glm-5.3 minimax-m2.7] and [minimax-m2.7] through both the
load-time repair and the journaled upgrade. Against an ungated rerouting
rule it fails: the connection is split, or its kind flips to anthropic
with preset_id opencode-go-anthropic. It passes here.
TestOpenCodeGoQwenModelsKeepBothRoutes pins the Qwen models to both
routes.
The project menu above the composer lists every "project" node from
GetProjectTreeSnapshot. Pinned SSH workspaces are project nodes too
(root "remote-project:<host>:<workspace>", Remote set), so they showed
up with the local folder icon, and picking one sent that synthetic root
to SwitchWorkspace, which stats it as a local path and fails.
The menu now drops nodes that carry a remote reference. SSH projects
remain reachable from the sidebar and the menu's "Remote connection"
action. This is a different path from #10755, which fixed remote tab
selection after restart and does not touch this menu.
Test: composer-workspace-context.test.tsx adds a remote node to the
snapshot. Before: AssertionError "remote projects never appear among
local folders to switch to". After: PASS; tsc and eslint clean.
Fixes#10891
Root cause: shellrun kept a child's bytes as a Go string unchanged. On a
Chinese Windows install, console tools such as tasklist and find answer
in cp936, so the bash tool result held invalid UTF-8 and JSON turned
every bad byte into U+FFFD on the way to the transcript. The model and
the user saw mojibake instead of the command's output or its error.
Fix: both buffers decode through encoding.DecodeOutput, the one decoder
for process output, and each passes the edges it really cut:
- the failure tail keeps the last N bytes, so tailWriter records when it
dropped bytes and passes Cut{Head: cut};
- combined output is decoded whole with Cut{} until it crosses the cap;
after that the head lost its end and the rolling tail its start, so
they are decoded apart with Cut{Tail: true} and Cut{Head: true} on
either side of the truncation notice.
Output that is not UTF-8 is decoded from the raw bytes. The shell
package's own edge trimmers (completePrefix/completeTail) are gone:
they trimmed edges nothing had cut and removed the GBK lead byte of
output that starts with a CJK character.
Tests:
- TestRunForegroundDecodesCodePageOutput runs a real shell that prints
GB18030 bytes and exits non-zero.
- TestBuffersDecodeCodePageOutputStartingWithCJK covers GBK that starts
with a CJK character in the tail and in combined output, uncut and cut.
All of them fail on origin/main-v2 and pass with this change.
Fixes#8213
On a Chinese Windows, cmd.exe reports a missing command in the console
code page (GBK), not UTF-8. The stdio transport kept the stderr tail as
raw bytes, so an MCP startup failure such as "'read-only-mysql-mcp-server'
is not recognized as an internal or external command" reached the settings
page as U+FFFD mojibake and the user could not see why the server failed.
The stderr tail now goes through encoding.DecodeOutput, the one decoder
for process output. The buffer keeps the last 16 KiB, so only its head
can be cut: tailBuffer records when it dropped bytes and passes
Cut{Head: cut}. The end is never cut, because it is whatever the server
last wrote. Output that is not UTF-8 is decoded from the raw bytes, so a
GBK tail that begins with a CJK character keeps that character.
Tests:
- TestStdioFailureDecodesCodePageStderr starts a real child that writes
GBK to stderr and exits.
- TestStderrTailDecodesTheServersCodePage covers GBK that starts with a
CJK character, both uncut and cut, plus UTF-8 cut mid-character.
All of them fail on origin/main-v2 and pass with this change.
Refs #7857
DecodeOutput trims a partial rune only at an edge the caller cut. Output
that nothing cut but that stops inside a character still has invalid raw
bytes, so it went through charset detection and fell back to GB18030,
and the whole output turned into mojibake: "编译成功…正在运行" plus half
of "中" read as "缂栬瘧鎴愬姛…". This is common in real use:
- a process killed on timeout mid-write;
- a persistent shell's partial() read;
- a block-buffered pipe.
The output is now read as UTF-8, less the final incomplete rune, when
two things hold: that rune is the only invalid part, and a multi-byte
UTF-8 character has already appeared. Both are structural evidence. When
only ASCII precedes the half rune, nothing says UTF-8, so the bytes
still go through detection. The uncut GBK probes (价, 啊\r\n, ok 洹,
啊 ok) are unchanged.
Test: TestDecodeOutputReadsUncutUTF8EndingInPartialRune fails before
this change with the mojibake above and passes after it. It also pins
ASCII plus half a rune (read as a GBK pair) and uncut, tail-cut and
head-cut GBK.
Refs #6488
The history panel sends no kinds, and the desktop filled that with
user_text, assistant_text, tool_input and tool_error. tool_output was
indexed but never searched, so a result the user saw in a conversation
(for example an MCP server listing) produced no "Content matches".
The panel's default now searches tool_output in place of tool_error.
The index writes a tool_output document for every tool result, errors
included, so tool_error would only list failed results a second time.
The agent-facing history tool keeps its own narrower default.
Searching tool output multiplies candidates, and the snapshot builder
reloaded a session file whenever consecutive bm25-ranked hits came from
different sessions, so interleaved hits cost one full file read each.
The builder now collects the needed rows per session in a first pass,
reads each session once to cut its snippets, and emits hits in the
catalog's rank order in a second pass.
Tests:
- TestHistoryContentSearchDefaultsCoverEveryToolResultOnce: before,
"zulumarker" (a normal tool result) returned no items; after, each
query returns exactly one tool_output hit.
- TestHistorySearchBuildReadsEachSessionOnceAndKeepsRankOrder: four
hits alternating across two sessions. Before: 4 session loads, want 2.
After: 2 loads, hits in rank order.
- 40 sessions x 80 tool results of 4-6 KB, term only in tool output:
first page 7.09s before, 0.89s after (single local run).
Problem: a desktop session watched through the IM bot bridge re-sent the
same approval or question card to the chat every time the desktop replayed
the waiting prompt while reconciling a tab (#9156).
Root cause: botBridgeHub.observe enqueued a watcher message for every
ApprovalRequest/AskRequest, and a replay carries the same kind as the
original request.
Fix: skip the watcher message when the event is marked Replayed; the pending
prompt is still remembered so a reply from the chat routes to the tab.
Test: TestBridgeReplayedApprovalPushesOnce fails before the change with an
unexpected second notification and passes after; the other TestBridge*
tests pass.
Problem: while an approval or ask card waited for input, Windows kept
raising "Approval needed" toasts, one about every minute (#9156).
Root cause: the desktop replays pending prompts whenever it reconciles a tab
(backend status sync, tab switch, navigation), and Controller replay emits
through the same notification-wrapped sink as the original request. Nothing
on the event said it was a replay, so the notify sink treated each one as a
new request for attention.
Fix: the controller, which is the only party that knows it is replaying,
marks the events emitPendingPrompts re-emits with event.Event.Replayed, and
notify sends nothing for a marked event. The sink keeps no state, so a
controller rebuilt behind the TUI's shared sink, whose prompt ids restart at
"1", still notifies its new prompts.
Test: TestReplayedPromptDoesNotNotifyAgain fails on origin/main-v2 with
"notifications for one approval replayed 3 times = 4, want 1";
TestRebuiltControllersSharingASinkEachNotify fails against an id-set dedupe
with "notifications for two controllers' approval 1 = 1, want 2". Both pass
after, with the existing replay tests and the notify and event suites.
edit_file on a UTF-8 file carrying CJK text rewrote bytes it never touched:
continuation bytes of characters far from the edit came back as 84 31 A4 37.
Detect took a nil error from the GB18030 decoder as proof the file was
GB18030. The decoder never fails: it maps every invalid sequence to U+FFFD.
So any file that was not valid UTF-8 -- one stray byte is enough -- was read
as GB18030, and the read-modify-write tools (edit_file, multi_edit,
delete_range, write_file over an existing file), checkpoint restore and the
config migrations then re-encoded every unreadable sequence as the GB18030
form of U+FFFD.
Detect now names a charset only when decoding and re-encoding restores the
input byte for byte. GB18030 is tried first, then GBK, which restores what
GB18030 cannot such as CP936's single-byte euro 0x80, so such a file stays
editable in its own encoding; anything else is LossyUTF8, whose bytes pass
through untouched. The new Kind is appended because checkpoints persist it
by value.
GBK cannot hold every character, so Encode now returns an error instead of
falling back to UTF-8, which rewrote a whole CP936 file as UTF-8 when an edit
added a character such as an emoji. A legacy charset that cannot represent a
character answers an *UnencodableError naming it, its byte offset and the
charset, with ErrUnencodable as its identity; the file tools, checkpoint
restore and the config migrations refuse the write and leave the file as it
was. Fixtures encode through MustEncode.
Readers that hold only part of a stream say so rather than inherit the
whole-file answer:
- DetectFragment runs the round trip on the prefix the decoder consumes when
not at EOF, so a character the cut split is dropped structurally. grep's
8 KiB peek, read_file's 256 KiB sample and the desktop's 2 MiB file preview
use it; the preview's next page starts at the split character, and lossy
text is shown as text rather than as binary.
- DecodeOutput is the one entry point for bytes that are only displayed.
The producer says which ends its bound cut (Cut{Head, Tail}); UTF-8 is read
without a character split at a cut end, and an uncut end keeps its bytes,
since a GBK pair there is output rather than half a rune. What no charset
restores is still read as GB18030. Hook output passes its tail cut when
truncated.
- DetectAndDecode decodes a legacy charset once; the edit tools and
DecodeToUTF8 use it instead of detecting and decoding separately.
Tests that fail before this change (on the base or an earlier revision of it)
and pass here:
- TestE2EEditKeepsBytesOfUTF8FileWithStrayByte: e4 b8 ad became
e4 b8 84 31 a4 37 outside the edit (fails on origin/main-v2).
- TestDetectUTF8WithStrayByteIsNotGB18030 (fails on origin/main-v2).
- TestWritesRefuseCharacterCP936CannotHold (edit_file, multi_edit,
write_file): nil error and the whole CP936 file rewritten as UTF-8.
- TestDecodeOutputTrimsCutUTF8Edges: UTF-8 cut at the front read as mojibake.
- TestDecodeOutputKeepsUncutCodePageEdges, TestDecodeHookOutputKeepsUncutCodePageEdges:
GBK pairs at an uncut edge (bc db, b0 a1 0d 0a, 6f 6b 20 e4 a1,
b0 a1 20 6f 6b) were dropped as partial UTF-8 runes.
- TestEditKeepsCP936FileWithEuroByte: an edit adding Chinese wrote UTF-8
into a CP936 file.
- TestGrepGB18030PeekWithoutNewline, TestGrepGB18030PeekCutMidCharacter:
no matches past a peek cut mid-character.
- TestReadFileGB18030SampleWithoutNewline: garbled text past the sample.
- TestReadFileGB18030TruncatedWithoutNewline,
TestReadFileGB18030TruncatedMidCharacter (desktop): the 2 MiB preview of a
GB18030 file was garbled or ended in U+FFFD.
- TestDecodeHookOutputReadsTruncatedCodePageText,
TestDecodeHookOutputReadsCP936Euro: mojibake for cut or euro-carrying cmd.exe output.
- TestEncodeRefusesCharacterTheCharsetCannotHold, TestDetectGBKWithEuroByte,
TestDetectFragmentDropsTheSplitCharacter, TestDecodeOutputReadsCutCodePageText:
the API's contract.
A Responses turn could end with a tool call the model issued and never
run it. The stream reader dispatched a function call only when its
function_call_arguments.done or output_item.done event arrived; a relay
that forwards the added and delta events but not the done events (or
sends deltas without the item id) left the call open, the terminal
response.completed ended the attempt, and the turn committed as a clean
answer with no calls. The run then finished ok after one round.
The terminal response's output array is the response's own list of what
it issued. On response.completed and response.incomplete, every
function_call item there that the stream did not already close (matched
by call id) is now completed from that item; one whose status is set and
is not "completed" is left alone, so a call truncated at the output
limit is still not dispatched. An item listed without a call id is not
dispatched, and a call id listed twice is dispatched once.
The output array now decodes one item at a time. Decoded as a typed
slice, a single item with a non-string field (arguments sent as an
object by a non-conforming relay) failed the whole terminal event, which
was then skipped and the turn ended with unexpected EOF; the reasoning
snapshot reader shared that exposure and now reads the same per-item
decode. A call with no name, such as an arguments.done event that named
no item, is no longer dispatched as an empty call; it stays open for a
later event to name. The done branches share one completion helper.
Test: output_calls_wire_test.go drives the real HTTP stream reader. On
origin/main-v2 the "done events never came" and "deltas without item id"
cases fail with calls = nil; both pass after the change, and the
no-duplicate and truncated-call guards pass on both. The
unreadable-item, empty-call-id, duplicate-call-id and
unnamed-arguments.done cases fail before the per-item decode and the
skips, and pass after.
go test ./internal/provider/... ./internal/agent/ passes.
Root cause: none of the five StepFun presets (stepfun, stepfun-responses,
stepfun-anthropic, stepfun-api, stepfun-api-anthropic) set context_window,
and 1.x has no model-level fallback, so a StepFun session ran with a zero
window. agent.Options documents that ContextWindow <= 0 disables automatic
compaction, and the context gauge has nothing to show, so the conversation
grew until StepFun refused it ("maximum context length is 262144 tokens").
Fix: every StepFun preset declares 262144, the limit the API enforces for
all models they list. Installed entries that still carry no window are
brought forward on load and on load-for-edit, only when the preset id, kind,
official host (stepfun.com or stepfun.ai) and served models all still match;
a custom endpoint, a foreign model or any window the user set is left alone.
Test: internal/config/stepfun_context_test.go fails on origin/main-v2
(stepfun context_window = 0; stepfun-anthropic/step-3.5-flash context_window
= 0 after LoadForRoot) and passes with this change. go test
./internal/config/ ./internal/boot/, go vet, gofmt, golangci-lint and
repolint are clean.
Fixes#9129
A task call may name a child model without a provider, e.g.
"deepseek-v4-flash". The sub-agent resolver passed that straight to
Config.ResolveModel, which answers a bare name with the first provider that
lists it. With the official DeepSeek entry listed before a relay serving the
same model, a child of a session running on the relay was silently sent to
the official API instead.
The model-override resolution moves out of the Build closure into
subagentModelSelection, and a bare name the parent's provider serves is
qualified with that provider first, in both the runtime resolver and the
identity the usage record reports. A provider name, a provider/model
reference, or a model only another provider lists resolve as before.
Test: TestSubagentBareModelStaysOnParentProvider fails on origin/main-v2
(selection for "shared-flash" = "first/shared-flash", want
"relay/shared-flash") and passes after the fix; go test ./internal/boot/
passes, repolint clean.
The context panel's "Runtime" stat froze between model requests: during
long tool execution it kept showing the value from the last usage event,
so a two-hour session could read a few minutes.
Root cause: the host accumulates runtime per turn and adds the running
turn's share only at the moment a snapshot is taken, while the panel
refetches that snapshot only when a usage event arrives. Nothing refetches
while tools run, so the number stood still.
Fix: ContextPanel now reports finished-turn runtime (elapsedMs) and the
running turn's start (activeTurnStartedAt), read under the same telemetry
lock. The panel and the context ring popover add the running share locally
and tick once a second while a turn is active, without polling the host.
Test: TestContextPanelSplitsRuntimeIntoCompletedAndRunningTurn (desktop Go)
does not build on main-v2 and passes after. context-panel-runtime.test.tsx
fails on main-v2 ('10s' !== '15s', value frozen across 60s with no usage
event) and passes after. Frontend run-tests: 477 suites passed; tsc clean;
repolint clean.
listDirForWorkspaceTarget sorted entries by DirEntry.IsDir and kept a
non-directory only when its lstat mode was regular. A symlink is neither,
so every link vanished from the file panel and from the "@" reference
menu, even one pointing at a folder or file inside the workspace.
Containment was also lexical only: workspacePathForBase joins and cleans
the path but never resolves links. So ListDir and ReadFileForTab, given a
path through a link, listed and previewed whatever sat behind it outside
the workspace, while the chat's "@" references already refuse that
through canonicalPathWithin.
A link entry is now listed as its target only when canonicalPathWithin
places that target inside the real workspace. A link that dangles or
leaves the tree stays out. So does one that resolves onto the folder
being listed or one of its ancestors: that folder would contain itself,
and the panel's compact-folder probe would follow it without a click.
ListDir refuses a folder whose real path is outside the tree.
ReadFileForTab refuses a workspace path whose real location is outside
it; a path that does not exist still reports its stat error.
Session-authorized external folder refs are unchanged.
Test: TestWorkspacePanelListsOnlyLinksInsideTheTree covers in-tree folder
and file links, out-of-tree folder and file links, a link to /etc, a
dangling link, "loop -> ." and "docs/up -> ..". On origin/main-v2 it
fails: the in-tree links are missing, ListDir("etc") = 72 entries,
ListDir("out-link") = 1, and ReadFile previews out.md,
out-link/shared.md and etc/hosts. It passes with the fix, and the other
listing and preview tests still pass.
Fixes#5914
Under a fake-ip resolver (Clash / mihomo) every public host answers with a
routable A from 198.18.0.0/15 beside a unique-local AAAA from fd00::/8.
The direct dial path refused the whole host as soon as any resolved address
was on the SSRF blocklist, so web_fetch failed for every site with
"refusing to fetch internal address ... (resolves to fdfe:...)".
Root cause: the guard treated one blocked address as a verdict on the host,
although the dial is pinned to a resolved address rather than the name. A
blocked address in a mixed answer can simply be skipped; the connect still
only ever reaches an address that passed the check. A host whose addresses
are all blocked is refused exactly as before.
The direct path moves into dialVettedAddress, which tries the allowed
addresses in resolver order, and the lookup becomes a package seam so the
test can supply a resolver answer.
Test: TestWebFetchDialsAllowedAddressBesideBlockedOne resolves a name to
[fdfe:dcba:9876::152, 127.0.0.1] and fetches from a loopback server. On the
base it fails with "refusing to fetch internal address api-docs.example
(resolves to fdfe:dcba:9876::152)"; with the fix it passes.
TestWebFetchRefusesHostWithOnlyBlockedAddresses keeps the all-blocked
refusal. go test ./internal/tool/builtin/, go vet, go run ./tools/repolint and
golangci-lint are clean.
A Service-owned session drops durable message bodies from its live
projection at open and after every recovery checkpoint. The writer's only
duplicate check for message/complete looked at those bodies, so it saw
only the ids accepted since the last checkpoint. A completion that reused
an older id (the approved-plan turn admitting its host message under the
user's reserved id did exactly this before bb6b7f608) was accepted and
written. Every full replay then refused the log as damaged: the session
could not be opened, and archive failed too, because archive proves the
target exists by taking a full snapshot.
The writer now keeps the set of live message ids beside the projection.
It is seeded from every open path, carried in the recovery checkpoint
(projection version 8, so older checkpoints rebuild once from the log),
and consulted before a commit is accepted, so a reused id is refused
with ErrDuplicateMessageID however long ago it became durable.
Logs already on disk keep the first message for a repeated
message/complete in every reader (projection, bounded startup including
the model workset, the checkpoint tail, catalog, history index with its
turn summaries, search), matching #10647's rule for replacements and what
the writer would have refused.
Archive and restore no longer read content to prove existence: a damaged
store is still movable to and from the trash. A damaged store reaches the
frontend as session_damaged instead of an unclassified failure, with a
message that says the session's file is damaged and can still be moved to
the trash.
The toast is now a no-drag region in the Electron shell. It sits over the
titlebar drag region, which swallowed clicks on its "show details" toggle.
A workspace below a Box Drive mount on Windows failed every config load
with "resolve project config path ...: The system cannot find the path
specified", whether or not reasonix.toml existed.
Root cause: resolveConfigAccessPath resolves the physical path through
pathidentity.Resolve (GetFinalPathNameByHandle on Windows) before it
confines a project config link to its directory. Below a cloud-drive
reparse point that resolution fails while the files themselves open
normally, and the failure was treated as fatal even when reasonix.toml
was a plain file or absent.
Fix: when resolution fails and the config entry itself is absent or a
non-link file, use the logical path. Such a file lies in its own
directory whatever its ancestors resolve to, so the confinement check has
nothing to decide. A link still has to resolve, so an unresolvable link
is refused as before.
Test: path_access_test.go stubs resolution to fail below a mount
directory. Without the fallback both LoadForRoot cases fail with the
reported "resolve project config path" error; with it they load (and the
project value is applied), and a reasonix.toml link under the same mount
is still refused.
remark-gfm defaults to singleTilde: true, so a reply such as
"500~1000 or 2000~3000" struck out everything between the first tilde and
the second. The shared remark policy, used by the live renderer, the
markdown worker and session export, now passes singleTilde: false; a
doubled ~~text~~ still strikes.
Test: markdown-pipeline.test.tsx renders two ranges plus one ~~...~~ span
on both the live and the worker path. Before: 4 FAIL (2 <del> each,
ranges not literal). After: 88 passed. pnpm test (476 suites), tsc for
the app and test configs, and repolint are clean.
A quoted paragraph was wrapped to the full terminal width and then
shifted right by the "▎ " rail (plus any list indent), so every full
row ran two or more columns past the edge. The terminal soft-wrapped
the overflow, leaving a stray rail on one row and the last characters
of the line on the next — most visible with CJK text, where every row
is filled to the edge.
renderBlockquote now lays the quote's body out at the width left after
its prefix and restores the renderer's width afterwards, so nested
quotes and quotes inside list items narrow once per level.
Test: TestBlockquoteRowsFitTheRenderWidth renders top-level, nested,
list-indented and Latin blockquotes at widths 20/41/80 and asserts no
row exceeds the width. It fails on the base (an 84-column row at
width 80, 24 at width 20) and passes with the fix.
Every native build waited on the input job, which spent 2 of its 2.5
minutes building the embedded docs and running the cache guard. Neither
is an input to a build. They move to their own job that runs alongside
the builds and gates only the seal; source CI, version and candidate
identity still resolve before anything builds.
Before approval the publish run proved each payload file's provenance
with one gh attestation verify after another, a network round trip per
file: 2.5 of its 6 minutes. The same check now runs eight at a time; any
file that fails still fails the step.
The candidate spent most of its 48 minutes compressing a 580 MB payload
with NSIS LZMA, a single-threaded pass of 5-6 minutes, three times per
architecture's worth of work on the critical path:
- the Windows build job built an unsigned installer and portable archive
that nothing uses when the release is signed (windows-sign rebuilds both
from the signed payload): ~8.5 minutes of the arm64 build. Signed
release builds now stop at the signing payload
(REASONIX_WINDOWS_PAYLOAD_ONLY); every other caller packages as before.
- windows-sign signed and packaged amd64, then arm64, so the two
compressions ran back to back (5.6 + 5.1 minutes). The finalize script
now runs in three phases: Certum signing of both payloads in the one
session, both packages built at once in separate worktrees (no
credentials involved), then Certum sealing of each installer in turn.
Without FINALIZE_PHASE it still does all three.
Installer bytes and compression settings are unchanged.
Expand every wildcard in the expected shard artifact pattern so CodeQL accepts the rerun contract test. The test still compares each shard report name with the workflow download pattern.
#10858: desktop 1.39.0 on Windows never showed a window. Every sandboxed
Chromium child exited with STATUS_BREAKPOINT, the GPU process crash-looped
until the browser quit, and the launcher reported that startup was not
verified within 30 seconds; --no-sandbox started it.
An allow entry for a specific AppContainer package SID on the DLLs next to
the executable does this (studio #10435 isolated it). The retired Windows
sandbox of 1.38.8 to 1.38.10 granted its package read/execute with
inheritance on the directory of the tool it launched, so such entries stay
on machines that ran it.
The shell now asks the service, synchronously and before Chromium starts
its first child, to remove allow entries naming a specific app package from
the tree the shell runs from (reasonix-desktop -strip-package-grants -app).
The judgement reads SID structure only; only allow entries go, so access
only narrows; an entry inherited from above is cut by protecting the object
that inherits it, leaving the parent untouched. Packaged Windows builds
only. If a removal is refused and the renderer dies before the window
shows, the shell names that cause instead of exiting silently.
internal/packagegrant is studio's package unchanged.
Verified on Windows 11 with the 1.39.1 build and the SID from the studio
recipe: the stock build shows the reported startup_failed dialog with no
window; the patched build starts and lists sessions with the grant on the
app directory and with it inherited from the install root (root and
service untouched). internal/packagegrant's Windows tests pass there.
A legacy transcript opened from the sidebar runs in place as a preview:
sends are refused, so it never holds anything its source lacks. It was
still persisted like any runtime. Moving to the imported session flushed
the preview's events and rewrote the transcript and meta (a resumed
preview carries the current system prompt, so the bytes differ), which
broke "the original file is kept" and made the import report the source
as updated and offer a branch right after it finished. The tab meta
writer rewrote the source .meta on open as well.
Every desktop path that persists a runtime (navigation, rebuild,
takeover, topic archive, removal, shutdown) and the tab meta writer now
skip a historical preview. Reproduced on Windows: transcript, event log
and both indexes stay byte-identical through preview, import and
navigation, and the update check reports unchanged.
A tab opened from a legacy row published only its transcript path, while
the row is listed by its source identity, so the renderer could not tell
they were one session and added a second row titled with the raw file id
and stuck on "indexing". The runtime node now carries the same source
identity (path and head through desktopSourceKey), and the existing
overlay marks the listed row open instead.
A headless `reasonix run`/`-p` binds a fresh v4 store in the project's
sessions-v4 root exactly like an interactive conversation, and nothing in
the store says it was a one-shot run. The desktop's historical catalog
lists every canonical store it finds there, so each agent-driven
`reasonix -p` became a permanent row labelled with its raw 32-hex id and
the Legacy badge.
The run kind is now persisted at creation: session.CreateOptions and the
v4 manifest carry an optional `kind`, the controller's
EnsureHeadlessRunSessionPath binds fresh stores with
kind=headless-run, and runAgent uses it. SessionInfo projects the kind
and the desktop's canonical catalog drops such stores from the sidebar,
the history list and the historical management listing. The store stays
resumable through --resume/--continue. The field is omitempty in the
manifest, so older binaries ignore it and keep reading these stores.
No header.json is written, so desktop ownership logic is unaffected.
Test:
- internal/cli TestRunPrintRecordsHeadlessRunKind drives runAgent -p
against a fake OpenAI endpoint; with runAgent still calling
EnsureSessionPath it fails with `store kinds = [""]`, and it passes after.
- desktop TestHistoricalCatalogOmitsHeadlessRunStores; without the
catalog filter it fails listing the store as a row labelled
0123456789abcdef0123456789abcdef in the sidebar, history and management
lists, and it passes after.
Stores created before this change carry no kind and stay listed.
A new conversation in a folder with many sessions disappeared from the
sidebar as soon as another conversation was selected.
Root cause: while any of a folder's session directories is scanning,
pending or degraded, ListProjectTopics answers with complete=false.
ProjectTree kept the last complete window by returning early for such
a first-page refresh, before mergeIncompleteProjectTopicPage ran and
before the page's keys reached itemKeys. The window renders only rows
whose key is in itemKeys plus the active row, so a new conversation
showed only while it was active. Once the user switched away it was no
longer active and not accepted, so it vanished until a complete page
arrived. With few sessions a full reload follows quickly; with many,
each refresh reconciles every catalog target and the Global scan stays
incomplete long enough, or permanently when a directory is degraded.
Fix: drop the early return. An incomplete first page goes through the
existing merge: residents keep their complete-page rows and order,
only newly discovered keys are added, and the old snapshot and cursor
are retained as before.
Test: project-tree-loading.test.tsx gains a case where a busy folder
(12 rows, 5-row window) receives an incomplete refresh carrying a newer
conversation. On base it fails ("a conversation discovered while the
catalog is incomplete is accepted into the window": A-0 is first, the
new row is absent); with the fix it passes. All 476 frontend suites
pass, tsc --noEmit is clean, repolint is clean.
`reasonix review` runs inside the checkout it reviews, which is untrusted.
Two routes let that checkout decide what review executes or asks for:
- The tool registry was built from the merged config, so a checkout's
reasonix.toml `[tools.search] engine = "rg"` + `rg_path = "tools/rg"` made
review's grep execute the checkout's own binary (unsandboxed on Windows),
and its `[sandbox]` chose the bash sandbox mode.
- The review skill resolved with project scope, so a checkout's
`.reasonix/skills/review/SKILL.md` replaced the built-in prompt and its
allow-list (e.g. adding web_fetch under an attacker-written prompt).
Review now builds its tool registry from the user-only config
(config.LoadUserConfigReadOnly) and resolves its skill from a store with no
project scope, so only built-in and user-level skills are candidates.
Credential env names from the merged config are still registered for env
filtering.
Two paths made a 1.38 user's sessions look gone on the first 1.39 launch,
both reproduced on Windows with data written by the 1.38.3 desktop:
- With no tab to restore, the Global folder started collapsed, so its
first page was never requested and the sidebar showed no sessions at
all. The folder now starts expanded when nothing is open; a manual
collapse still wins.
- The chat 1.38 opens at launch is saved as the active tab. When it was
never sent it has sidecars (.meta, goal state, inbox lock) but no
transcript, event log, turn log or subagent, and restore classified it
as recoverable legacy artifacts: every launch opened on a startup error
and "unable to load session". Such a tab is now dropped as stale
presentation, on both the unmapped and the archived-mapping path. No
file is touched.
#10843 gave task, skill, fleet and background subagents their own hook
session, but three agents that call tools still ran with no hook runner:
- the dual-model planner (plannerOpts in boot.build) runs read-only
research tools such as read_file, and its Options carried no Hooks, so
a PreToolUse deny on reading a secret did not cover the planner's reads;
- `reasonix review` builds its subagent outside boot and passed Options
with no Hooks at all;
- the desktop settings-page try run (App.TrySubagentProfile) runs
read_file, search and policy-wrapped bash in the active workspace, also
with no Hooks.
Options.Hooks is the whole ToolHooks surface, so each of these agents now
runs PreToolUse, PostToolUse and PostToolUseFailure around its tool calls,
PostLLMCall on each model turn (its stdout replaces the reasoning stored in
that agent's session) and PreCompact before its compaction (its stdout is
added to the summary guidance), not only tool hooks. No agent reads hooks
while building its prompt or tool schema, so the cache-stable prefix is
unchanged.
Hook sources and sessions:
- Planner: hook.Runner.ForRole("planner") keeps the session's resolved
hooks, cwd and spawner, and derives its session ID as
"<parent session>:planner" each time a hook fires, so a SetSessionID on
the parent (/new, /clear, resume) reaches it and hooks keyed on
session_id do not merge sessions.
- reasonix review: loads only the user's own hooks, global settings.json
and installed plugins. hook.LoadOptions gains SkipProject, which leaves
out <root>/.reasonix/settings.json, so reviewing an untrusted checkout
never executes a command that checkout configures. Its hooks keep the
checkout as cwd, which is what they inspect, and run with
NoDefaultCurrentDirectoryInExePath=1 (Runner.WithoutCwdCommandSearch), so
on Windows cmd.exe never resolves a bare `python` in a user hook to a
python.exe the checkout ships. Each run is its own session,
"review:<random>".
- Desktop try run: loads project and global hooks the way a chat session
in that workspace does, under "try-subagent:<random>".
boot.NewCommandHookRunner now takes the hook.LoadOptions to resolve, so a
caller states its hook sources. The hook package doc no longer claims
project hooks load only for trusted projects.
The guardian is left as is. On main-v2 nothing outside internal/guardian
calls Session.Review or ReviewVerdict, the only paths that run its agent,
so it executes no tool. Wiring it is not a one-liner either: its agent is
built inside guardian.NewSession, which would need a new parameter across
29 call sites for a path no production code reaches.
Tests:
- TestBuildRunsPreToolUseInsidePlanner (through boot.Build): a plan-only
turn whose planner calls read_file is blocked by PreToolUse, and the hook
sees "<parent>:planner" where <parent> is the ID the UserPromptSubmit
hook got. After ClearSession a second turn's planner follows the new
parent ID.
- TestReviewCommandRunsOnlyUserHooks: with a global deny hook and a
project hook, the global one blocks read_file and the project one never
runs; two runs get distinct review:<id> sessions.
- TestTrySubagentProfileRunsWorkspaceAndUserHooks (desktop): both the
project hook and the global deny run for the try run's read_file under
a try-subagent:<id> session, and the file contents never reach the model.
- TestReviewHooksNeverResolveCommandsAgainstTheCheckout: every review hook
spawn carries NoDefaultCurrentDirectoryInExePath=1 with the checkout cwd.
- TestForRoleDerivesSessionFromParentAtFireTime and
TestWithoutCwdCommandSearchReachesEverySpawnOfItsChildren (internal/hook).
Docs: GUIDE.md and GUIDE.zh-CN.md describe which hooks each read-only
child runs and under which session ID.
Refs #9255
* docs(release): prepare v1.39.1 notes
Summary:
Generate a bilingual, product-focused draft from merged pull request metadata. Reuse the selected release-bound PR when one is available.
Verification:
Validate the catalog, citations, bilingual fields, and rendered GitHub release notes before committing.
* docs(release): describe the session catalog fix by what users saw
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: esengine <[email protected]>
This reverts commit 1aa4a1fb3.
The 1.x line keeps the CLI's install channels (npm `reasonix`, the
Homebrew cask and the CLI update pointers) until 2.x is stable; the
studio line now publishes them only when STUDIO_PUBLISHES_CLI is set.
Without this, 1.39.1 would ship its fixes while npm and Homebrew stayed
at 1.39.0.
Restored workflow steps are pinned to the same full-length SHAs the
rest of main-v2 uses, and the contract test that cuts release-npm.yml at
setup-go matches the pinned form.
A pull request authored with GITHUB_TOKEN never starts other workflows, so
the weekly acknowledgments PR never reported the checks branch protection
requires and could only land by bypassing it. The step now uses the
dedicated ACKNOWLEDGMENTS_TOKEN secret when it is set, and falls back to
GITHUB_TOKEN otherwise.
The contributor table also listed github-actions[bot]; bot accounts are
now filtered before the top 20 are taken (30 are fetched to keep 20).
Checked by running the script against the live API: the previous version
writes one github-actions row, this one writes none.
TestScheduleSnapshotCoalesces and TestCloseTabNoResurrectionFromAutosave
polled the session file for 10s and then gave the loop another fixed 10s
to go idle. Neither bound is anything the autosave loop promises: one pass
can wait sessionFileLockWait (5s) on the compatibility lock, ReplaceFile
retries a locked destination for up to ~1.5s per replace, and a failed pass
is retried twice more. On a loaded hosted Windows disk the first save of a
test binary missed both windows.
CI evidence (desktop-windows-go-group, windows-latest):
- 36162070209/108163266006: TestScheduleSnapshotCoalesces 59.13s; the
first saveLocked entry is logged ~50s after the test started, both waits
expire, then "open ...session.jsonl: The system cannot find the file
specified" followed by "file already closed".
- 36144229234/108103306720: same test, 25.12s, file never had the content.
- 36164121404/108168781324: TestCloseTabNoResurrectionFromAutosave, 19.97s,
same symptom with a single TurnDone.
The two trailing errors are teardown, not the save: once the idle wait gave
up, the remaining cleanups closed the controller's event store and removed
the TempDir under the still-running loop.
Diagnostic runs on windows-latest (run 36221953315, CI-shaped
`go test -run ^Test[Q-S] ./...` x4 and ^TestC x4) put the same tests at
0.7-1.8s with every phase I/O-bound and no scheduler stall, versus 0.2s
unloaded: latency scales with the disk, which the test does not control.
Fix: wait for the loop to go idle - its termination is guaranteed by the
bounded waits and retries above - and only then read the file once. The
only cap left is the test binary deadline. Every autosave test in the file
uses this, including the retry test's former 5s bound.
Reproduction (test-only, not committed): fileutil.CrashPoint sleeping 1s
before every durable write/replace, then running the original test body.
- origin/main-v2: TestScheduleSnapshotCoalesces 3/3 FAIL and
TestCloseTabNoResurrectionFromAutosave 3/3 FAIL, with the CI lines
(app_autosave_test.go:139/:497 and :92) and the same teardown pair
"no such file or directory" then "file already closed".
- this change: 3/3 and 2/2 PASS, no teardown errors.
Unmodified stress: the five autosave tests -count=200 clean,
-race -count=20 clean.
The macOS native browser smoke (electron/scripts/browser-runtime-smoke.mjs,
job desktop-macos) failed with
RpcError: cancelled: browser request cancelled or expired
at Timeout.<anonymous> (main.cjs:8:34337)
10 times across 6 PRs and main-v2 (e.g. runs 35905864950/job 107334745447,
35924725548/107398259097, 36143253798/108099365388,
36144902003/108109092740, 36166759544/108177697220,
35847129273/107137474207). main.cjs:8:34337 is the 10s timer in
BrowserRecorder.start that waits for the recorder renderer's "started"
message; passing runs reach "started" in under 2s.
Root cause: the recorder asked getDisplayMedia for { frameRate: 25 }. The
fixture page is static, so tab capture emits only its first frame and the
refresh frame a newly attached video sink requests. The first frame can
reach the track before the <video> sink connects; when the capturer answers
the sink's refresh with a frame that trails the first by under 40ms, the
track's 25fps limiter discards it. Nothing else is ever produced, the video
never reaches HAVE_METADATA, the canvas is never painted, no chunk arrives
and start aborts at 10s. Instrumented stalls show exactly this in
MediaStreamTrack.stats: deliveredFrames 1, discardedFrames 1, totalFrames 2,
unchanged for 9s, while the page, capture host and capturePage stay healthy.
Fix: request the capture track without a frame-rate cap. The output rate is
already set by the canvas paint interval (40ms) and requestFrame, so the cap
only ever discarded frames.
Reproduction (local macOS, Electron 44.2.0): a test-only hook attaches the
video sink immediately after the track's first frame, the interleaving from
the stalls.
before (origin/main-v2): 9/40 recordings abort at 10s; all 9 are exactly
the runs whose track discarded a frame, the 31 without a discard pass.
after: 0/200 fail, 0 discarded frames, although 115/200 runs had a second
frame within 40ms of the first.
Full smoke minus the foreground-input block, 6 parallel workers under CPU
load: before 4 recorder aborts in 540 runs, after 0 in 192.
recorderRenderer.test.ts pins that the capture track is requested without a
frame-rate limit; it fails on the previous renderer.
TestLegacyTopicArchiveVisibility failed in desktop-windows-go-group (I-P)
8 times across 7 branches, including main-v2, e.g. runs/jobs
36141013690/108092129567, 36158613176/108151570952,
35910881048/107351239668, 35979938768/107570884455:
topic_archive_visibility_test.go:41: session catalog did not stop
before directory cleanup
(the two older runs, before the cleanup check existed, show the same
handle as "TempDir RemoveAll cleanup: ... rebuilt-catalog.sqlite: being
used by another process").
Root cause, two parts:
- stopSessionCatalog dropped the catalog pointer, then gave up at its
deadline while Catalog.Close kept running in the background. Nothing
kept the in-flight close, so the next stop found nothing to wait for
and reported true while the SQLite handle was still open. This is
reachable in production: a rebuild whose stop times out restarts the
watcher, and the next rebuild's stop then reports the old handle
released. In the test, the mid-test stop's result was ignored and the
old owner's cleanup check passed vacuously.
- The test bounded the whole orderly shutdown (reconcile drain, worker
drain, TRUNCATE checkpoint, close) by 1s of wall clock. Under load the
checkpoint alone was measured at up to 1.87s, so the bound was below
the observed latency of work the test does not control.
Fix: each stop records its close; the close runs to completion in the
background and waits for any earlier one, and stop reports true only
once every handle the App opened is released. The reconcile wait still
ends at the deadline, since a reconcile can wait on discovery nobody
resumes once stopped. The test waits on that condition with the shared
sessionCatalogTestDeadline hang detector, and checks the mid-test stop.
Reproduction:
- TestStopSessionCatalogWaitsForEarlierTimedOutClose holds a read
snapshot so the closing checkpoint waits out its busy timeout, stops
with a 1ms deadline, then stops again: origin/main-v2 fails 20/20
("stop reported the catalog stopped while it was ready"); fixed passes
-count=200.
- 24 concurrent GOMAXPROCS=1 processes of TestLegacyTopicArchiveVisibility
-count=4: origin/main-v2 17/24 processes fail with the CI line (48
hits); fixed 0/24.
desktop-windows-go-group (I-P) ran `go test -run ^Test[I-P] ./...` and
reasonix/desktop hit Go's default 10m per-package alarm:
run 36165720818 / job 108174359373 (main-v2) FAIL reasonix/desktop 600.369s
run 36161103402 / job 108158948888 FAIL reasonix/desktop 600.159s
run 36166824914 / job 108179614742 FAIL reasonix/desktop 600.199s
The reruns of the same commits passed with reasonix/desktop at 348.6s
(job 108167833614) and 401.9s (job 108309233796).
Root cause: no test hangs. At the alarm the running test had been going
for 1-15s (TestLegacyTopicArchiveVisibility,
TestOpenCanonicalSessionAfterWorkspaceRemovalFromNativeSurface,
TestProjectTreeMigratesNewCLISessionAfterProjectDirMarker), and the
largest gap in the product log (66-86s after the parallel-NN.jsonl
writes) covers TestParallelDesktopTabs... through
TestPurgeMigratedSourcesBusyWriterResumesFromTombstone, which take 27.9s
locally: the same ~3x Windows slowdown seen everywhere else. I-P holds
651 tests and is the heaviest group by far (other groups: 66-244s on
the same runners, Q-S up to 398s), so on a slow hosted runner its
package run alone crosses 600s.
Fix: split I-P into I-M (367 tests, 69s locally) and N-P (314 tests,
67s locally) in the partition driver and in both matrices that use it
(desktop-windows-go-group and desktop-go-race). The partition test now
also holds the race matrix to the driver's group list so the two cannot
drift. The alarm stays at Go's default: it is the hang detector, and
each group now has roughly twice the headroom under it.
Reproduction (macOS, CGO_ENABLED=0, same alarm scaled to local speed;
I-P takes ~138s here, so 120s stands for 600s on a runner ~5x slower,
where the failing jobs were):
before: go test -run '^Test[I-P]' -timeout 120s .
panic: test timed out after 2m0s, FAIL 120.874s
after: go test -run '^Test[I-M]' -timeout 120s . ok 78.2s / 86.7s / 96.8s
go test -run '^Test[N-P]' -timeout 120s . ok 67.1s / 86.5s / 71.1s
TestHistoricalRegressionShutdownDrainsStartupWorker panicked with
"close of closed channel" on Linux desktop-go, killing the whole
reasonix/desktop package run:
run 35905864950 / job 107334745459 (main-v2 3a0a3d91)
run 36160668180 / job 108157764097 (main-v2 c0b98408)
run 36152302442 / job 108137534322 (PR, rerun passed)
The panicking goroutine is the catalog discovery started by
requestHistoricalCatalogWithContext, emitting from
discoverHistoricalSessions' deferred notify (historical_catalog.go:125).
Root cause: after tabs are restored the startup worker calls
requestHistoricalCatalog (spawning a discovery worker) and then
emitProjectTreeMetadataChanged. Both publish a project-tree change, and
both are legitimate: the discovery replaces a nil catalog with an empty
one, so it reports a change. The test's hook closed `entered` on every
call, assuming exactly one emission before shutdown. Normally the
discovery is still running when the test cancels the coordinator and it
returns without notifying; when the test goroutine is descheduled after
<-entered, the discovery finishes first, emits, and closes `entered`
again. The product's behaviour is correct; the test asserted on an
ordering it does not control.
Fix: the hook signals `entered` once and parks every caller until
release. Stop must drain both workers either way, so the assertion that
shutdown waits for the parked publisher is unchanged in meaning.
Reproduction: a copy of the test with a 2s pause between <-entered and
stopHistoricalImports fails every run on origin/main-v2 with the CI's
exact stack; with the fixed hook it passes -count=20. The real test
passes -count=200 -cpu 1,4 and -race -count=10 over TestHistoricalRegression*.
Flake: desktop-browser-group (transcript), step "Test native browser
foreground runtime" (xvfb-run node electron/scripts/browser-runtime-smoke.mjs).
Four failures, all at the first cross-origin click, all with
RpcError: page_not_ready: input target has not reached a stable
rendered frame; observe the page again data: { kind: 'page_not_ready' }
run 35833016485 / job 107091102092 (main-v2)
run 35983968809 / job 107583562246 (main-v2)
run 35954377998 / job 107490367842 (main-v2)
run 35866024386 / job 107199844899 (PR fix/transcript-render-isolation)
Root cause: dispatchMouseInput refuses a mouseMove, before dispatching
anything, when pageInputReady does not see two animation frames with
unchanged geometry within 500ms. The refusal is typed and retryable; its
contract is "observe the page again". The smoke sent its first click right
after iframe onload, which is not a rendered-frame boundary, and treated the
refusal as fatal. In every failing log the run ends ~0.7s after Electron
starts versus ~0.34s to the first click in passing runs, consistent with the
500ms unpainted-frame deadline on a slow runner. The product refusal is
correct; the smoke asserted a timing it does not control.
Fix: every native smoke click goes through clickObserved, which follows the
contract: on page_not_ready it re-observes (snapshot + ref resolution, or
the zoom-point read) and retries within 5s; any other failure, or the
refusal at the deadline, still fails. A refused child-frame attempt retires
its OOPIF session, so the shared-session assertion counts from the
observation that clicked, and the idle-release assertion counts from the
attachments before idle.
Reproduction: REASONIX_BROWSER_UNSETTLED_INPUT=1 keeps the fixture page
moving until the first input is refused, then stops it before
re-observation, and asserts a refusal happened. The Linux CI step now sets
it, so the refusal/re-observe path runs on every build.
Local, macOS (Electron 44.2.0; screen locked, so run with
--disable-gpu-vsync --disable-frame-rate-limit to keep frames flowing):
origin/main-v2 + hook only: 0/20 pass, 20/20 the CI page_not_ready line
fix, REASONIX_BROWSER_UNSETTLED_INPUT=1: 29/30 pass (1 failure in the
later recording-takeover phase, "cancelled: browser request cancelled
or expired", after all input sites had passed)
fix, default: 15/15 pass
Root cause: canonical (v5) sessions had nowhere to keep their permission
preset. The legacy BranchMeta sidecar is refused for them, and neither the
Session Service projection nor workspace presentation carries a preset, so
the value lived only on the tab. OpenSession applied the source surface's
live preset to the target session (prepareCanonicalControllerRuntime read
it from the outgoing controller). Session B inherited A's
danger-full-access, and a surface built from New Chat forced workspace-write
onto every session opened into it. The renderer had the same leak one layer
up: its composer profile table is keyed by tab, and the push that follows a
navigation sent the previous session's preset to the new one.
Decision: keep the preset per canonical SessionID in a desktop-owned record
(desktop-session-presets.json next to desktop-tabs.json), with no change to
the session log schema. Opening a session, and restoring tabs at startup,
use only the value recorded for that exact SessionID. A session with no
record gets the new-session default. The value on the surface is never
carried over.
Recording rule: a record is written only by SetPermissionPresetForTab, the
one preset setter fenced by session. The caller passes the SessionID of the
permission snapshot it read; under the tab's turnStartMu, which also
serializes navigation, the call is refused with
reasonix_error:permission_session_changed (errPermissionSessionChanged)
unless that id equals the live controller's session. The record is keyed by
that same session and the file is written outside App.mu. The revision alone
cannot fence this: it is per controller, so two freshly opened sessions share
a number. SetToolApprovalModeForTab and SetModeForTab name no session and so
never record; saving tabs, submissions, composer-profile sync and draft
settings never record either.
Fix:
- sessionPresetStore exposes restore, record and forget. A failed write
removes the file, or empties it in place when the directory refuses the
removal, so a restart opens every session at the default; the full set is
written again on the next record or forget.
- The renderer passes the snapshot's sessionId to SetPermissionPresetForTab.
On permission_session_changed it re-reads the tab and does not retry the
choice on the session the tab now shows.
- Whenever a tab's canonical SessionID changes (syncTabSessionIdentity:
New Session, clear, legacy continue, rebuild), the tab and its controller
take that session's own record or the new-session default.
- New Session on a blank tab that is reused drops the blank session's record,
resets it to the default, and advances the controller's permission
revision (Controller.InvalidatePermissionSnapshots), so a snapshot read
before the reuse cannot record a choice for the session handed out as new.
- A fork tab starts at the new-session default rather than the source tab's
preset.
- The canonical open path passes the target session's restored preset to
prepareCanonicalControllerRuntime, and startup restore does the same for
tabs bound to a SessionID. Legacy tabs keep their existing sidecar path.
A session that is permanently deleted drops its record.
- docs/desktop-migration inventory regenerated for the new signature and
line positions.
- Composer profiles in the renderer record which session they were read for
(owner). A profile, or a meta snapshot, whose owner belongs to a different
session than the tab's current one is discarded, not reconciled.
Tests:
- TestCanonicalSessionPresetFollowsSessionAcrossNavigationAndRestart and
TestRestartDoesNotTrustSurfacePresetForUnrecordedSession fail on
origin/main-v2 (session-B / session-A tab preset = "danger-full-access",
want "workspace-write").
- TestNewSessionInTabDoesNotInheritSourcePreset: A=full, New Session in the
same tab -> the new session is the default, has no record, and stays the
default after restart through desktop-tabs.json and after reopening; A is
still full.
- TestNewSessionOnBlankTabDropsItsEarlierChoice and
TestForkStartsAtNewSessionDefault cover the reused blank and fork paths.
These three fail without the recording rule above with
tab preset = "danger-full-access", want "workspace-write".
- TestUnreadablePresetRecordsRestoreDefault: a corrupt record file restores
the default.
- TestStalePresetChoiceDoesNotLandOnNavigatedSession: snapshot A, open B
(same revision), then set full access with A's snapshot -> refused with
errPermissionSessionChanged, B not recorded, B stays at the default. The
same scenario through the unfenced signature fails with "stale choice for
session-A accepted on session-B" and B recorded as danger-full-access.
- TestPresetWriteFailureInReadOnlyDirFailsClosed: full access recorded, the
config directory made read-only, read-only chosen -> restart does not
restore full access. Without the in-place truncation it fails with
narrowed to read-only, write failed, restart restored "danger-full-access".
- TestSnapshotBeforeBlankSessionReuseIsRefused: snapshot the blank tab, New
Session reuses it, set full access with the old snapshot -> refused and
nothing recorded. Without the revision advance the set is accepted and
recorded as danger-full-access.
- The desktop preset tests pin a host that offers every preset
(control.SetPresetSandboxForTest), so they assert the same thing on a
runner without an OS sandbox, where read-only and workspace-write are not
offered. Pinned to "no sandbox", three of them fail with
permission preset "read-only" is unavailable: shell sandbox requested but
unavailable on this host.
- TestUnfencedModeSettersDoNotRecord: SetToolApprovalModeForTab and
SetModeForTab leave no record.
- frontend permission-preset-session-fence.test.tsx: a navigation between the
snapshot and the set neither applies nor records the stale choice, the
refusal is not retried, and the tab is re-read.
- frontend composer-profile-session-navigation.test.tsx runs the real
projection, lifecycle and controller-profile hooks through A -> B -> A.
The v1.38.9 desktop release page reads "This vv1.38.9 desktop release":
resolve already emits the version with its v prefix, and the disclosure
template added another one.
Normalize the prefix in the step instead of assuming either convention,
so the public text stays correct if the resolver output ever changes, and
assert the shape in the release workflow contract test.
Root cause: legacy conhost with a CJK TrueType font (the default on
Chinese Windows) sizes East Asian Ambiguous runes by the glyph, so it
draws a font-dependent subset two cells wide: · … “ ” ‘ ’ — – ● → ↑ ↓
◆ ° × and Greek/Cyrillic letters among them. The TUI and its renderer
count all of them as one cell, so every full-width row carrying k of
them is k cells too wide on screen: conhost wraps it, the alt-screen
frame grows past the window and scrolls, and later repaints land on
shifted rows, which is the stacked-frame garbage in the report.
Decision: measure instead of guess. When stdout is a console, a
never-activated screen buffer of that console (same font) measures
each non-ASCII rune the layout counts as one cell, once, via
WriteConsoleW and a cursor read-back. A rune the console draws wider is
swapped, after layout, for a one-cell stand-in both sides agree on:
Unicode's halfwidth variant when it has one (→ becomes U+FFEB), else
the OS US-ASCII best-fit (· becomes ., “ becomes "), else the OS
default character. Each candidate is itself measured before it is
used. The swap is rune for rune at equal counted width, so applying it
to the finished frame changes no row's layout; escape sequences and
OSC payloads pass through untouched. A pseudoconsole (Windows Terminal,
VS Code, SSH) measures these runes as one cell, so nothing is replaced
there; other platforms get a nil fit and are unchanged.
Test: TestChatFrameFitsConhostMeasuredWidths renders the real chat
View at 120 columns with the width table measured on a Chinese
Windows 11 conhost and asserts no row draws wider than the terminal.
With the substitution disabled it fails:
row 0 draws 121 cells on conhost, terminal has 120
With the fix it passes on macOS and on the Windows box. In a real
conhost window, the unfixed binary's status row wraps ("EFFORT aut" /
"o") and the header scrolls away; the fixed binary keeps the 30-row
frame intact. Under ConPTY the fixed binary still emits the original
glyphs.
Fixes#10540
Root cause: a force-killed turn lost state in two ways. Sessions opened
from a legacy transcript write the model-visible .jsonl only on the 30s
autosave tick or at turn end, so a kill before the first tick dropped the
prompt from the model's context even though the display ledger kept it.
Event-log (v3) sessions record the prompt on admission, but streamed
assistant text reaches the log only when the message completes, so a kill
mid-stream lost the partial reply entirely.
Decision (option A): two bounded changes, no change to the cache-stable
prompt prefix.
- The agent announces a new advisory checkpoint boundary right after the
user message is admitted. A controller without an event store saves the
legacy transcript there (one extra save per turn); stores that already
hold the message ignore it, and a failed save only logs.
- The controller accumulates the open stream's deltas and, on the existing
30s autosave tick, appends an optional stream/checkpoint event carrying
the same local-only interrupted record a cancelled stream leaves. It
holds commitMu so the checkpoint can never land after the commit of the
message it describes. The projection keeps the newest checkpoint of the
open turn and drops it on any later message commit or turn boundary;
restart recovery upserts it before turn/end interrupted. A completed
stream therefore never shows twice, and the record stays out of
provider-visible history.
Tests (internal/control/turn_kill_durability_test.go), each killing a
child process with os.Exit and no TurnDone, then reopening:
- TestKilledLegacyTurnKeepsItsPrompt fails on main-v2 with
"prompt of the killed turn lost after restart"; passes with the fix.
- TestKilledTurnKeepsItsPartialOutput fails on main-v2 with
"partial output of the killed turn not durable after reopen: 0 rows";
passes with the fix, and the partial text stays out of ModelMessages.
- TestCompletedStreamSupersedesItsCheckpoint and
TestKilledAfterCommittedStreamShowsItOnce fail on main-v2 with
"no partial-output checkpoint was written while the stream was open";
with the fix the completed reply appears exactly once, including after
a kill between its commit and turn end. Disabling the supersede rule
makes the latter fail with the checkpoint replacing the reply.
Root cause: Electron 44 picks its Wayland Ozone backend by itself in a
Wayland session. On drivers whose EGL platform init fails there (Kylin V10
SP1 with the Mali stack in #10369) the GPU process exits and the app dies
with 139 before a window appears; the same machine works with
--ozone-platform=x11. Nothing in the launch chain chose a backend. The
.desktop entry runs reasonix-launcher, which runs reasonix-desktop, whose
bootstrapShell builds the Electron argv and starts it detached, so that
spawn is the one place that controls the shell's argv. The flag has to be
in argv: Chromium selects Ozone before the main script runs.
Decision: default to X11 whenever it is available. bootstrapShell prefixes
--ozone-platform=x11 on Linux when the session is Wayland
(XDG_SESSION_TYPE, else WAYLAND_DISPLAY) and DISPLAY is set.
REASONIX_OZONE_PLATFORM=wayland keeps native Wayland, x11 forces X11, auto
is the default. A pure Wayland session without DISPLAY and an X11 session
are unchanged, and an explicit --ozone-platform or --ozone-platform-hint
argument wins. Relaunches through the launcher re-decide; Electron's own
relaunches reuse process.argv and keep the flag.
Test: TestBootstrapShellChoosesOzonePlatformFromSession drives
bootstrapShell with a fake shell that records its argv, over the session
table. On origin/main-v2 it fails:
shell argv = ["--flag"], want ["--ozone-platform=x11" "--flag"]
With the fix all nine cases pass.
Root cause: handleFinalResponse accepts any reasoning-only clean stop as
the end of the turn unless RequireVisibleFinal is set, which only
internal callers set. When the model runs tools and then stops with
reasoning only, the tool results never get a visible synthesis and the
turn goes idle until the user sends another message (#9758).
Decision: a narrow structural retry. When this turn ran tools, no
visible assistant text follows the last tool result in the transcript,
and the stop is reasoning-only, the existing empty-final retry runs
once. A second reasoning-only stop is accepted. A reasoning-only answer
to a turn that ran no tools is still accepted as final, as #9788 set
out. The judgement reads structure only: usedAnyTool, empty content,
and message position; no wording is inspected.
Tests: TestRunRetriesReasoningOnlyStopAfterToolRoundOnce and
TestRunAcceptsSecondReasoningOnlyStopAfterToolRound drive a scripted
provider through Agent.Run. On the base both fail with
"provider calls = 2, want tool round + reasoning-only stop + one retry";
with the fix they pass. The existing no-tool reasoning-only tests
(TestRunAcceptsReasoningOnlyFinalAnswer and siblings) pass unchanged,
as do internal/agent and its direct dependents.
internal/crashreport gained process_unix.go (built on darwin) with the CLI
fatal-crash capture, but the package was not in darwinRoots, so pull
requests touching it skip the macOS lane. release-control's
"every darwin-specific package is inside the pull-request group" test
caught it on the main-v2 push; the pull request itself skipped
release-control by path.
scripts/macos-go-tests.test.mjs: 2 pass / 1 fail before, 3 pass after.
The Linux reasonix-desktop service was the one Go binary in the package
built with cgo left at its default. On the ubuntu-22.04 release runner that
links it against glibc 2.35, so the desktop-v1.39.0 tarball's service names
/lib64/ld-linux-x86-64.so.2 and needs GLIBC_2.32/2.34: it will not start on
glibc 2.31 systems (Debian 11, Ubuntu 20.04, Kylin V10, UOS 20), while the
launcher, migrator, CLI and update helper beside it are already static.
Nothing on Linux needs cgo: every `import "C"` file is darwin-only, sqlite
is pure Go, and the Linux StatusNotifierItem tray is pure-Go D-Bus. The
tray files were gated on cgo, though, so a plain CGO_ENABLED=0 build would
have swapped in the no-op tray; they now select the D-Bus tray on linux
regardless of cgo.
- scripts/desktop-build.sh builds the Linux service with CGO_ENABLED=0.
- verify.mjs reads the ELF program headers of every Go member of the Linux
tarball and .deb and refuses one that names a program interpreter, so the
release build fails instead of shipping a glibc floor again.
Test: lib.test.mjs adds ELF parsing and archive-member cases; on the base
it fails at import ("does not provide an export named
'checkStaticGoMembers'"), with the fix 23/23 pass. Against real binaries,
checkStaticGoMembers reports the desktop-v1.39.0 release service as
"reasonix-desktop is dynamically linked (interpreter
/lib64/ld-linux-x86-64.so.2)" and returns no errors for the service built
by the fixed script path. `GOOS=linux CGO_ENABLED=0 go list` now selects
tray.go, tray_loop_external.go and tray_health_unix.go instead of
tray_nocgo.go; linux vet/test compile and darwin vet pass.
Fixes#10372
The sidebar is meant to show Global only while it holds conversations
or has a custom title, or while no project exists. The registry merge
in mergeCanonicalWorkspaceShellsFromProjection broke that: it re-adds
every workspace whose registry record is Visible, and
ensureDesktopWorkspace marks Global Visible the first time anything
targets it, including opening the Global composer draft
(OpenSessionDraftForTarget), even when no session gets created. From
then on Global stayed in the sidebar with no conversations, whether or
not any project existed. The reporter saw this on one machine and not
on another because it depends on whether Global was touched before the
first project was added, not on the OS.
Projects are added on purpose, so their Visible flag stays
authoritative. Global is never added on purpose. The registry now keeps
it only while it owns an unarchived session or a source mapping.
Test: TestGlobalFolderAfterProjectCreatedFollowsGlobalConversations
- base: draft_target_without_a_conversation fails with
"Global folder listed = true, want false after a project was created"
- fix: all three cases pass (untouched, draft target, blank
conversation, which still keeps Global)
The transcript's parsed-markdown cache was keyed on `${item.id}@${revision}`.
A settled assistant message carries a live id (p3) while the controller owns
it and a history id (m:<messageId> / he:<entryId>) once the store projection
renders it, so the same text parsed under one id missed under the other and
went back through the markdown worker.
A parse is a pure function of its source text, so the cache is now keyed by
the text's content revision and a hit additionally requires the stored source
to equal the requested text, which keeps a 32-bit revision collision a miss.
The row id still opts a row into caching; it no longer names the entry.
Test: markdown-natural-flow renders a settled body under cacheKey "p3",
remounts it under "m:msg-1", and asserts no second worker parse. On base it
fails with `4 !== 3` ("a row re-keyed by the history projection reuses the
parse of the same text"); with the fix it passes. transcript-store covers the
LRU budget, pinning and the collision miss (382 passed).
An explicit --model whose provider has no key failed with
`provider "deepseek-flash" requires DEEPSEEK_API_KEY`. Provider keys
resolve only from <Reasonix home>/.env (storedCredentialValue), so the
message sent users to export the variable in their shell or VM-wide
environment, which Reasonix never reads. It also labelled the model ref
as a provider. Reported through `reasonix acp --model deepseek-flash`
from the VS Code extension in #10458.
The error now names the model, the variable, the credentials file path
and `reasonix setup`, and says shell environment variables are not read.
Test: TestResolveModelForCLIMissingKeyNamesCredentialFile sets the key
only in the process environment. On the base it fails with
error "provider \"deepseek-flash\" requires REASONIX_CLI_TEST_KEYLESS_KEY"
does not mention ".../.reasonix/.env"
and passes with the fix; go test ./internal/cli/ passes.
loadProjectsFile runs on every sidebar read and catalog pass. Its
normalizers (project dedupe, pinned list, sidebar order, organization
sidecar) find a root with projectIndexByRoot/projectRootInList, and each
comparison went through pathidentity.Same, which resolves both paths from
scratch. One load therefore cost O(projects^2) identity resolutions; on
Windows each resolution opens every ancestor directory and queries its
case-sensitivity flag, which matches the Procmon trace in #10738 (about
57k Read-Attributes opens on every project root, 533k
QueryCaseSensitiveInformation calls, almost no file content read).
pathidentity gains a pass-scoped Matcher that resolves and stats each
distinct path once; Same delegates to it, so a single comparison behaves
exactly as before. The project-file normalizers share one matcher per
pass. Nothing is cached across passes, so a directory whose identity
changes is seen fresh on the next load.
Test: TestLoadProjectsFileResolvesEachRootLinearly loads 24 projects
(pinned, sidebar order, organization sidecar, one symlink alias) and
counts identity resolutions. With only the counting seam in place it
failed: "loading 24 projects resolved path identity 4400 times, want at
most 100"; with the shared matcher it passes, and the alias still folds
into its target. TestMatcherResolvesEachDistinctPathOnce checks the
matcher agrees with Same on every pair while resolving each path once.
Refs #10738
Problem: a turn such as "按方案实现登录模块,数据库结构不要改。" had every
write_file/edit call refused with "blocked: the current constraints forbid
state mutation"; a follow-up "continue" turn then wrote fine.
Root cause: ParseConstraints set ForbidMutation from a phrase table over the
user's text (hasGlobalMutationBan) and inspected only the words after the
negated verb. Chinese places the object before the verb, so a scoped
protection ("X 不要改", "其他不要改", "The schema: do not modify") left an empty
tail and was read as a global read-only order for the whole turn.
Fix: retire the wording-based mutation ban. ForbidMutation is now set only
structurally, by plan mode or an inherited parent constraint; the user's
prose still reaches the model as an instruction. This matches the studio
line, where the prose parser has already been retired.
Test: internal/agent TestUserProseDoesNotBanWritesForTheTurn drives a real
Agent.Run whose model issues write_file. On the base it fails for every case,
e.g. "write_file executed 0 times, want 1; result="blocked: the current
constraints forbid state mutation"". With the fix it passes, and
TestPlanModeStillBlocksWrites confirms plan mode still refuses the write.
go test ./internal/runtimepolicy ./internal/agent ./internal/control pass.
Fixes#10482
desktop-windows-package has failed on main-v2 since 27d99bbdb with
"windows-upgrade-fixture: startup imported or recreated the legacy
session". ffdd87ef8 (#10759) made a restored legacy tab report its
historical source, so the tab now shows the import banner, and made the
banner action a real PrepareSession import. The first-launch acceptance
ran with -PrepareHistoricalSession, clicked that action, and the fixture
then found the import it forbids. Startup itself still imports nothing.
- The first launch no longer prepares. The UI wait accepts the import
banner beside a visible transcript body when it was not asked to
prepare, and the fixture keeps its no-import verification.
- The restart launch prepares. `verify --phase restart` now requires
exactly one legacy SourceMapping for the fixture's legacy path whose
session is the global workspace's only session, no operation left
open, the active tab carrying that session and topic, the authored
legacy history unchanged, and the topic databases and backups intact.
- ci-paths routes the acceptance scripts and the upgrade fixture to
the packaging surface. They run only inside desktop-windows-package,
and a pull request that changed only them skipped that job.
Test: TestWindowsUpgradeFixtureImportsLegacyOnlyWhenPrepared restores
the saved tab, prepares its source through PrepareSession and opens the
result with OpenSession. The restart verification fails on the untouched
state, and the first-launch verification fails on the imported state.
Against the previous fixture the test fails with:
prepared verification passed before the legacy source was imported
and with that pre-import check removed it reproduces the CI error:
restart: startup imported or recreated the legacy session
TestVerifyPreparedImportRequiresOneCommittedLegacyImport covers the
rejected import shapes.
ci-paths.test.mjs pins the new packaging routes; it fails on the
previous pattern for scripts/test-windows-upgrade-startup.ps1.
UsageStatsPanel is lazy()-loaded, so UsageStatsPanel.css ships only in a
lazy chunk. verifyFillWidthOptions opened a fresh page and went straight
to the theme gallery, so a too-broad rule in that stylesheet was never on
the page it measured: with the old global rule
`:root .settings-options.settings-options--content > button` placed in
UsageStatsPanel.css the check still passed.
The check now opens Usage statistics first and waits for its groups, so
the lazy stylesheet is loaded before the theme gallery is measured.
With the old global rule in UsageStatsPanel.css:
before: PASS chromium/windows fill-width theme preview options
after: AssertionError: chromium/windows/1600px: fill-width options span
their group (99.375 of 206px)
With the current scoped rule the full settings-layout bench passes.
Refs #9980
desktop-go-race is eleven parallel partitions of the whole desktop module
under -race, on every pull request that touches native code. The same
partitions already run without -race on each pull request (desktop-go and
the Windows groups), so the race sweep mostly repeated that work per PR head.
It now runs on pushes and manual dispatch only. The desktop aggregate gets
RACE_REQUIRED, which mirrors the job's own condition, so a pull request
expects the sweep to be skipped and still fails if it unexpectedly runs.
A data race introduced by a pull request is reported by the push run after
merge.
scripts/ci-workflow.test.mjs evaluates the job condition and the aggregate
expectation over every event/native/notes-only combination and runs the
aggregate script for the new pull-request cases; both fail against the
previous ci.yml.
routeEntry promoted MCP tools to prefer through two phrase tables.
looksLikeGitHub fired on "issue" or " pr " and then matched every tool of
any server whose name contains "github"; looksFreshData fired on "latest"
or "查一下" and matched every tool named *search*/*fetch*/*read*. Each MCP
tool is its own entry, and limitRouteCandidates caps only suggest
candidates, so one GitHub server with forty tools put forty prefer lines
in the capability-route block and the model spent turns declining them.
explicitMCP ("<server> mcp") had the same shape: naming a server selected
all of its tools.
An MCP tool is now routed only when the request contains the identifier
the host minted for it (mcp__<server>__<tool>). Anything less specific is
left to the semantic router, which already runs when deterministic routing
finds no strong candidate and caps what it appends.
TestRouteDoesNotFloodOneMCPServer builds forty github tools plus
search/fetch/read tools on other servers: on the base a GitHub question
routes 43 prefer candidates; with the fix it routes none, and naming
mcp__github__tool_7 routes exactly that tool.
Every SettingsOptions button takes flex: 1 1 0 with min-width: 0 and an
ellipsis. The usage statistics range and source groups are content-sized
inline-flex boxes whose width is the sum of their labels, so the equal
share handed spare width to short labels and cut every label longer than
the average. In Chinese that clipped "最近 14/30/90 天" and "远程工作台".
The usage statistics groups now give each option flex: 0 1 auto, so it
sizes to its label and still shrinks with an ellipsis when max-width
binds. The rule is scoped to those groups: other content-layout callers,
such as the theme gallery preview controls, make the group full-width
and rely on equal shares to fill it.
The settings browser gate opens the usage statistics page under a zh-CN
locale and asserts no range or source button is narrower than its label,
and opens the theme gallery and asserts each preview control's buttons
span their full-width group.
When tool hooks are configured, every depth-0 tool call holds a
whole-workspace parent write claim for the duration of Execute, including
run_skill/task. A writer-capable subagent that call starts (explore,
research, any non-read-only skill) asks the scheduler for a whole-workspace
writer slot at depth 0, which is not Nested, so the conflict with that
parent claim queues instead of failing. The claim is released only after
Execute returns, and Execute is waiting on the queue: the turn hangs with
no timeout.
The parent claim now carries an id. The tool call puts it into its Execute
context, and a subagent acquired under that context is not refused by that
claim, nor by it from Realize/MarkOpaque on the child's own writes, nor is
it ordered behind a queued whole-workspace writer (which that same claim is
blocking). Every other parent claim and every other request is scheduled
as before.
Test: TestDepthZeroDelegationIsNotBlockedByItsOwnHookParentClaim drives
executeOne on a depth-0 agent with hooks and a delegate tool that acquires
a whole-workspace writer slot and runs a depth-1 child write; on the base
it times out waiting on the claim. TestDelegationInsideParentClaimIsNot
QueuedBehindWritersThatClaimBlocks covers the queued-writer ordering.
Fixes#9688
A 400 whose message reads "Input length 1048826 exceeds the maximum
length 1048566" is a provider-confirmed context overflow, but the
context-limit parser had no shape for it. ParseContextLimitError
returned nil, SendWithRetry handed back a plain APIError, the user saw
"Malformed request ... likely a bug", and the agent's overflow recovery
never ran because AsContextLimitError found nothing.
The parser now accepts this shape alongside "prompt is too long: N > M",
under the same invariant: the input must exceed the maximum, or the
body stays unclassified.
Test: TestSendWithRetryInputLengthOverflowIsContextLimit serves the
reported body from an httptest server and asserts SendWithRetry returns
a ContextLimitError with prompt 1048826 and window 1048566; it fails on
the base with a *provider.APIError. TestParseContextLimitErrorInputLengthNeedsOverflow
holds the invariant for equal, smaller and zero inputs and for a 401.
A new scheduled task needs a title and instructions, but the editor
disabled Save while either was empty and said nothing about it. The
title is a borderless heading-style input whose placeholder reads like
a filled-in value, so a user who set the project, permissions and
schedule saw a dead Save button with no reason. Suggestions prefill a
title, which is why editing one of those did save.
Save is now enabled for a new draft; pressing it with a required field
empty does not persist anything, marks the empty field aria-invalid,
shows an inline message tied to it via aria-describedby, and moves
focus to it (switching back to the configuration tab if needed).
Test: "heartbeat editor required fields" in heartbeat-editor.test.tsx
renders a new task with instructions but no title and presses Save. On
the base the title is neither marked invalid, focused, nor explained;
with the fix all five checks pass.
A tag is a mutable pointer: whoever controls the action's repository can
move v7 to new code and every workflow here runs it on the next event,
with this repository's token and on its self-hosted runners. A commit SHA
cannot be moved.
Each reference keeps its tag as a trailing comment, which is the form
Dependabot's github-actions ecosystem (already configured) reads and
updates. The composite actions pulled in by reference
(attest-build-provenance, upload-pages-artifact, the Windows signing
setup) already pin their own nested actions, so the repository-level
"require full-length SHA" policy can be enabled once this lands.
Six workflow contract tests asserted the literal tag spelling
(upload-artifact@v7, setup-node@v7, attest-build-provenance@v3). They now
require the pinned form "@<sha> # vN": the major version is still checked,
and a reference that drifts back to a floating tag fails them.
The workspace file panel and its change watcher reused fileref.SkipEntry,
the "@" reference search's skip predicate. That predicate also skips
skipDirPaths, a table of this repository's own generated-output paths keyed
by workspace-relative path, so "tmp", "bin" and "stage" matched the
top-level directory of every user workspace and vanished from the panel,
and changes under them were dropped by the watcher.
Split the predicate: fileref.SkipBrowseEntry keeps only the name-based
noise (VCS stores, dependency trees, build outputs) and is what the panel
and the watcher use; SkipEntry, used by "@" search, still adds the
repository paths.
Tests: TestWorkspacePanelListsGenericTopLevelDirectories and
TestWorkspaceWatchSeesChangesUnderGenericTopLevelDirectories fail on the
base (tmp, bin and stage are missing from the listing and their changes
are skipped) and pass with the fix; TestSearchSkipsRepoOutputPathsThatBrowsingShows
keeps "@" search skipping tmp/.
TestSearchFileRefsFindsNestedBasename now holds the new contract: ListDir
shows bin/, stage/, tmp/ and desktop/frontend/wailsjs while "@" search
still skips them.
The Settings editor submits the request URL it displays, which for the
official DeepSeek Responses and Anthropic routes is the standard URL
derived from the base URL. IsOfficialDeepSeekSearchEndpoint treated any
non-empty request or chat URL as a custom override, so saving the provider
cleared WebSearch to nil, and nil reads back as on for official endpoints:
turning server-side search off never persisted.
A request or chat URL that normalizes to the base URL's standard route is
no longer an override; any other URL still is. The legacy Messages default
restore keeps skipping entries that carry an explicit request or chat URL.
Fixes#10145
Pressing up on the first line of the chat input recalls the previous
prompt and saves the draft so down can bring it back. Every key other
than up, down and enter reset that recall state, including pure cursor
movement, so up followed by right or Home and then down left the
recalled prompt in place and the draft was gone with no way back.
The recall now resets only when the input no longer holds the recalled
entry, i.e. when the user actually edited it; the edited text then
becomes the draft for the next recall, as before. Cursor keys keep the
recall session and its saved draft.
Because cursor keys no longer leave recall, recall is line-aware while
it is active: up moves to an older entry only from the first line of
the input and down to a newer entry or the draft only from the last
line; on any other line of a multi-line entry the textarea moves the
cursor instead of replacing the text.
TestSubmittedInputRecallKeepsDraftAcrossCursorKeys fails on the base
("down after cursor keys should restore the draft, got \"old\"") and
passes with the fix; TestSubmittedInputRecallEditedEntryBecomesDraft
pins that an edited entry is left alone by down;
TestSubmittedInputRecallMovesWithinMultiLineEntry pins that up, left,
up, down inside a recalled three-line entry keeps that entry and that
down from its last line restores the draft.
A long-running CLI session that died silently left nothing to diagnose.
The CLI only recovered panics on the main goroutine and never called
debug.SetCrashOutput, so a panic on any other goroutine or a fatal runtime
error (out of memory, concurrent map writes) went only to the console's
stderr, which is gone once the terminal closes. The TUI diagnostic log
also kept its first 4 MiB and dropped everything after, so a session that
ran for days lost exactly the entries leading up to the crash.
The CLI now mirrors runtime crash output into a per-PID file under the
Reasonix home, and the next start queues any dump left by a dead process
as a pending crash report (runtime classification and goroutine stacks,
panic value redacted). A panic already reported through recover detaches
that file first so it is not queued twice. The TUI log now rotates within
the same total bound: the live file holds half the budget and replaces a
single .prev.log sibling, so the newest diagnostics survive.
Tests: TestGoroutinePanicIsReportedOnNextStart re-runs the test binary
as a child that panics on a background goroutine, then asserts the next
start lists the report; it lists none on the base. TestTUIDiagnostics-
KeepLatestEntriesWithinTotalBound writes three times the limit and a
marker, and asserts the marker is on disk within the bound; on the base
only the head is kept.
agent.subagent_effort is a cross-model default, but when agent.subagent_model
is unset the subagent role inherits the execution model and the preflight
validated the global value as a hard requirement for it. Switching to a model
that does not declare that level (for example a custom OpenAI-compatible model
without supported_efforts) aborted Build with UNKNOWN_MODEL_REASONING before
any request was sent. Build also fed the same value into the task tool and
skill subagents, so skipping it at preflight alone would only move the failure
to the first subagent request.
inheritedSubagentEffort now decides once, from the execution model's reasoning
capability, whether the inherited global default applies. When it does not,
subagents fall back to the model's own default, Build emits a warning notice
naming the dropped value, and the persisted setting is left untouched so it
applies again on a model that supports it. agent.subagent_efforts.<profile>
and an effort paired with an explicit agent.subagent_model stay strict.
Tests: subagent_effort_inherit_test.go covers the preflight, the resolved
default, the Build notice, and the strict companions; the preflight and Build
cases fail on the base with UNKNOWN_MODEL_REASONING.
Refs #10503
resolveSessionQuery reported several matches as a bare sentence, so
neither caller could recover which conversations matched: headless and
TUI starts both printed "session query ... is ambiguous (N matches)" and
exited, with no ids to narrow the query by and no way to open any of the
matched sessions.
The ambiguity now carries identity: ambiguousSessionQueryError holds the
matched entries. resumeQueryTarget reads it with errors.As. On a terminal
the TUI start opens the session picker over just those matches. Headless
starts, and TUI starts without a terminal, still exit 1, but list each
candidate (time, the id --resume matches exactly, summary) and a hint to
rerun with one of those ids. The picker's menu building moves into
chooseResumeEntry so both paths share it.
TestHeadlessResumeAmbiguousQueryListsCandidates fails on the base (stderr
only carries the ambiguity sentence) and passes now;
TestInteractiveResumeAmbiguousQueryOffersOnlyMatches checks the chooser
receives exactly the two matching sessions and its pick is used.
StartTurnForTab admitted the submission on one controller, discarded the
SubmissionReceipt that SubmitIdentified returned, released the tab
admission locks, and then re-resolved the tab's controller to ask it for
the turn id. A controller rebuilt in that window (compaction, maintenance,
settings) cannot be relied on to hold the receipt, so an accepted turn was
reported to the user as "turn admission did not produce a durable turn id".
control.SubmitResult now carries the TurnID of the receipt, filled by
submitToTabResult from SubmitIdentified (and from a known-submission
lookup), and StartTurnForTab answers with it. The re-resolved lookup
remains only as a fallback for controllers that return no receipt.
StartTurnForTabWithDrafts uses its receipt directly as well.
Test: TestStartTurnForTabAnswersWithAdmittingControllerReceipt swaps the
tab's controller for one without the receipt while the turn starts; it
fails on the base with the reported error and passes with the fix.
A turn_done with status interrupted or recovery_required took the generic
"turn interrupted" branch in both renderers (the transcript projection and
the desktop reducer), which ran before the err branch and dropped err,
detail and diagnostic. Turns persisted by 1.38.7 as recovery_required with
an HTTP 402 therefore replay as an unexplained interruption.
The interrupted branch now also emits the provider_request_failed notice
when the turn carries an error whose diagnostic is not a cancellation. The
cancellation test reads the diagnostic kind, now named
provider.FailureKindCancelled, rather than the error text; a user stop still
shows only the interrupted notice.
Tests: TestInterruptedTurnKeepsProviderFailure (internal/transcript) and
interrupted-turn-provider-error.test.ts both fail on the base and pass here.
/rename gated on Controller.SessionPath() and wrote the legacy .meta
sidecar. Since the CLI binds exclusive sessions-v4 sessions, the legacy
path is never set, so "/rename <title>" always answered "no active
session to rename"; "/rename <n> <title>" refused every final-format
row outright.
The current session now resolves to its canonical identity when the
controller is bound to one and is titled through
Controller.SetSessionTitle, the same event owner the set_session_title
tool uses. A picked canonical row that is not the bound session goes
through session.Service.SetTitle. Legacy transcript paths keep the
sidecar write.
Test: TestRenameCurrentCanonicalSessionWritesTitleEvent and
TestRenameIndexedCanonicalSessionWritesTitleEvent in
internal/cli/rename_canonical_test.go run /rename against an exclusive
sessions-v4 controller and read the title back from the session
projection. Both fail on the base (RenameNoSession / the final-format
refusal) and pass with this change.
The crash overlay is the last surface a broken renderer paints, and it
offered only "Send report" and "Copy". Nothing on it could recover the
app, so users had to kill the process from the OS to get back in.
The overlay now carries a "Restart Reasonix" action. It resolves the
host's RestartApplication command at click time through the host
adapter, as the send action does, so it works without React or the
bridge module. When no desktop host is present or the relaunch is
refused, it reloads the renderer instead, which still recovers a
renderer-only crash. The button lives in its own module to keep
crash.ts inside its size budget.
Test: src/__tests__/crash-overlay-restart.test.ts paints the overlay
under jsdom and clicks the restart action. It checks that the host
relaunch runs once, and that a refused relaunch or a missing host falls
back to a reload. On the base the overlay has no restart action and 4
of 5 assertions fail.
Reasonix 2.x shares the session directory with 1.x and writes
<id>.wire.jsonl, <id>.adjudication.jsonl and <id>.execution.jsonl beside
its transcripts. 1.x classified them by their .jsonl ending, so every 2.x
session showed up as extra empty conversations in the resume picker, and
-c could land on one of them as the newest.
The #10759 change asked the controller whether it runs a legacy source
while metaForTab still held a.mu, which breaks the rule the rest of the
function keeps and that TestMetaForTabRevalidatesBindingAndReadsController
Unlocked holds: controller reads happen after the lock is released. Its
stub controller panicked there on CI. The tab's recorded source and head
are taken under the lock; the controller is asked once it is released.
#10755 grew session_workspace_api.go past the 800-line ceiling and added a
comment over the essay budget, and a test comment read as stage narrative;
repolint failed lint-code on all three. The selection block moves into
session_local_surface.go unchanged in behaviour, and the comments state
the constraint instead.
f9883482d made a login id name one file, but on Linux a backslash is an
ordinary character, so `..\evil` passed as a local name and
TestSaveAccountRejectsTraversalID has failed the ubuntu, macOS and race
jobs on main-v2 since. The same config directory can be read on Windows,
where that name climbs out of the account folder, so both separators are
refused everywhere.
checkAdoptedMigrationSource quarantines an adopted legacy source whose
fingerprint changed as a pending recovery entry. Restoring that entry
refused with "historical source changed; rescan before restoring" once
the source moved again, and nothing ever rescanned it, so the entry
stayed pending on every startup (#10759).
Restore now rebases the entry onto the fingerprint read under the source
lock and converts the version on disk. A source held by another writer,
missing, or unreadable is still refused. Because entries are keyed by
version, a version already restored settles every entry that names it:
the restore commit marks sibling entries for the same version restored,
RecordRecovery no longer re-quarantines a restored version on the next
startup, and rebasing onto a restored version opens that session instead
of importing it twice.
A saved tab kept in recovery with reason historical_source_pending is
restored onto a native legacy runtime. That runtime continues the
transcript in place but has no durable session store, and every desktop
send carries a submission ID, so the controller refused each one with
"durable submission identity unavailable". The tab offered a composer
that could only fail, and the banner's import action reopened the same
source in place instead of preparing it (#10759, #10760).
- Turn admission refuses an identified submission to a native legacy
runtime before it reaches the controller, with the typed session code
historical_source_pending carried in the not-accepted RPC error.
- Tab and runtime meta report the historical source for such a runtime,
so the frontend projects the tab as pending: the composer is disabled
and says to import before sending.
- The banner action prepares the source (PrepareSession) and opens the
resulting canonical session, reusing the source-update polling path.
- The controller's refusal is an exported sentinel,
ErrSubmissionIdentityUnavailable, instead of an anonymous error.
Text and reasoning deltas already merge in event.Coalesce (at most one
event per 16 ms window), but ToolProgress bypassed it: every pipe write of
a running shell command became its own transcript frame. A build printing
a few thousand short lines published ~1000 frames/s, so any renderer poll
cycle slower than ~250 ms overran the follower's 256-change queue, and
each overrun cost a full snapshot baseline in the desktop renderer.
Measured through the real controller follow path with a client that
applies the desktop acceptance rules and takes 400 ms per batch:
- 3000 reasoning/text deltas: 20 changes, 0 resets (already coalesced)
- 3000 shell output chunks: 6 resets in 2.3 s before, 0 after
(~160 changes, largest batch ~39 of the 256 allowed)
A ToolProgress event carrying only a tool ID and Output now merges with
the next one for the same tool under the same window and byte cap.
Progress with any other field (Verifying, sub-agent channel names,
parent ids) still passes through unmerged and flushes the buffer first,
so ordering and every consumer's append semantics are unchanged. The
burst identity is grouped into one deltaKey instead of more scalars.
The follower's queue bound is untouched: a genuine overflow still
requires a new baseline (TestFollowOverflowRequiresNewBaseline).
The shell waited 10 s for the desktop/shutdown reply, then polled
shutdownStatus for a fixed 10 s and declared shutdown_incomplete. A
service still closing after that window was reported as failed, and its
completed/success reply, arriving seconds later, was dropped as an orphan
response, so the user saw a failure dialog for a quit that succeeded.
The shutdown reply now stays awaited for the whole wait, so a late
completed result is taken as the verdict. After the reply window the
shell follows status: it keeps waiting while the service is alive and its
phase or outcome still advances, and fails only when the service exits
without success (service_exited), stays in one phase past the stall
window, or the explicit 60 s ceiling passes. Failures carry their code on
a typed ShutdownError, and the timing is injectable for tests.
A schema-2 session whose <id>.event-index.json no longer matches its log
(an older build appended without rewriting it, or the index is gone) is
projected with head_count from the sidecar but no catalog_heads rows, and
queued for repair. Repair then took the listing-index shortcut, returned
already_current without touching the head index, and marked the row valid.
The index stayed stale forever, so a fresh catalog generation held
sessions declaring heads it never had rows for: v10 with 0 head rows
against sum(head_count)=9.
Repair now skips the listing-index shortcut when a schema-2 log's head
index is stale, so the replay path rewrites the head index. The catalog
lands that session's head rows, head_count and selected head in the same
transaction that marks it valid, so a repaired row never names heads it
does not carry.
executeApprovedPlan runs the execution turn in the planning turn's
context, which carries the user message identity reserved for that turn.
The agent admits the host "plan approved" message under that identity, so
the session holds two messages with the user's id. The durable
message/complete is refused as a duplicate, and a later full
history/replace of the in-memory session can write the duplicate to disk,
after which the session can no longer be hydrated.
runComposedSyntheticTurn now reserves a fresh identity for the synthetic
message it admits.
A history/replace or legacy/import payload that repeats a message id was
accepted by the projection, but the history index re-indexes a replacement
without upsert and fails with "duplicate indexed message id". Every
transcript follow of such a session failed, so the desktop could never
hydrate it again.
Readers now take the first occurrence of each id in a replacement, in the
projection, history index, search index and transcript metadata alike, so
logs already on disk open and all views agree on one list. The first
occurrence is kept because the log already treats an id as claimed by its
first message: message/complete refuses a later duplicate.
Writers are refused at PrepareBatchContext with ErrDuplicateMessageID, and
message/complete's duplicate refusal now carries the same sentinel, so a
new duplicate never reaches the log.
Archiving a historical source whose session was already deleted returned
Committed with outcome already_removed but persisted nothing. The frontend
only dropped the row from memory, so after a restart the residual topic was
listed again. That row survives when the deleted session's purge receipt
carries no topic ownership, which earlier purge writers omitted, so
purgedCanonicalTopicIDs cannot retire it.
On already_removed the legacy source's topic now joins deletedTopics
through the existing projects-file helper, which every sidebar projection
honours. The topic is left listed when a non-deleted session or a pending
create still carries it, or another transcript beside the source carries
it without being adopted by a deleted session.
The global workspace is a singleton derived from the state root
(<home>/global-workspace), but workspace-state-v1.json stores its absolute
root. After a portable build or REASONIX_HOME moved, every ensure of the
global workspace saw a different root under the fixed "global" ID and
EnsureWorkspaceResolved refused it with ErrMutationConflict, so Global
failed to load and no global session could be created or opened.
A different stored root for global can only be where the data directory
used to be, so both EnsureWorkspaceResolved and ReconcileDiscoveredSession
now rebind global to the current root instead of conflicting, keeping the
old root in formerRoots. Canonical sessions recorded their CWD under the
old root; binding, restore, recovery choices and discovery accept a former
global root as global instead of treating it as a conflicting project.
An empty root still conflicts, and project workspaces are unchanged.
Legacy sessions-v4 stores keyed by the old global root's slug are not
rebased here.
The Ask card is collapsible: PromptShelf toggles collapse on any click
that does not land on a recognised control. The custom-answer row is a
div role="group" that focuses its input, so a click on its pencil or
padding both focused the input and bubbled up to collapse the card,
hiding the answer the user had just started.
Stop the row's click from reaching the card; the row is itself a
control. A new interaction test clicks the row outside its input and
checks the card stays expanded, and that the card's own chrome still
collapses it.
The Git graph dialog rendered in place inside the composer frame, and
.composer-workspace-frame--context sets isolation: isolate. Its fixed
backdrop and z-index therefore only ranked inside that stacking context,
and the right workspace dock, painted later as a sibling column, covered
the part of the dialog that extended over it.
Render the dialog through createPortal to document.body, as the other
modal dialogs do, so it stacks at the overlay layer of the whole window.
The workspace-context test now asserts the dialog is mounted outside
the composer tree.
Canonical /sessions rows expose a session through sessionId/hostId and
leave the legacy path empty, but the web UI's session list posted only
{path}. The server answered 400 "missing path or sessionId" and the click
did nothing visible, so no older session could be opened.
Send sessionId, hostId and name alongside the path so both canonical and
legacy rows resolve, and show a warning notice carrying the server's
status and reason when the resume is refused or the request fails.
Every preset change cancelled the active turn, so moving from
workspace-write to full access mid-turn threw away the work in progress.
The cancel exists so an approval granted under an older snapshot cannot
authorize work the new one forbids; a widening revokes nothing the turn
holds, so it has nothing to protect. Only a change that does not raise
permissionPresetRank cancels now, matching how background jobs are
already treated.
On an sshfs/macFUSE project root, pathconf(_PC_CASE_SENSITIVE) returns
EINVAL, and platformIdentityKey returned that error, so the workspace
could not be opened at all. An undeclared answer (EINVAL, ENOTSUP,
EOPNOTSUPP) now means case-sensitive, which keeps distinct names distinct;
other pathconf errors stay fatal. A statfs failure no longer fails the
lookup either: the volume is not known to normalize names, so NFD is
skipped.
The rules move into a platform-neutral darwinIdentityKeyBy with the
volume probes injected, so they are tested on every host; key_darwin.go
binds them to statfs(2) and pathconf(2).
Relays that re-serialize tool schemas into typed structs turn an omitted
"required" into null, and strict upstreams then reject the whole request:
"Invalid schema for function 'get_goal': null is not of type 'array'".
get_goal's schema has no required list, and neither do many MCP tools.
CanonicalizeSchema now adds "required": [] to every root object schema
that lacks one (including the no-argument fallback), so builtin and MCP
tools reach the provider with an explicit array. The effect test checks
every tool in the real provider request.
Cache impact: the tool schema bytes change once (get_goal gains
"required": []), so ToolsHash and PrefixHash move in both golden
baselines and the first request after upgrade misses the prefix cache.
Goldens regenerated with REASONIX_UPDATE_GOLDEN=1.
Disk projections ran in WAL mode with no journal_size_limit, so a WAL
that grew while a reader pinned an old snapshot kept its peak size after
every later checkpoint: a session catalog was seen at 22 MB of WAL beside
a 0.4 MB database. The limit is per connection, so it rides the DSN
pragmas to reach every pooled connection rather than one Exec'd pragma.
The session catalog also truncates its WAL on close; the checkpoint is
best-effort because another process's reader may still hold it.
Fixes#10714
In host mode the update handoff owner is the Electron parent
(updateHandoffOwnerPID returns os.Getppid()), but the superseded-update
check only recognised os.Getpid(). The service therefore took its own
freshly prepared transaction for a stale missing-backup record, archived
it to repair/legacy-updates on shutdown, and the install helper then
failed to claim a handoff whose transaction no longer existed. The owner
is now recognised as either this process or its parent.
A record whose rollback backup still existed was never retired, and the
health commit re-hashes that backup, so a .DS_Store written by Finder
made it fail forever with a digest mismatch and pinned every later
update. Once the running version has reached the target and the health
commit cannot prove the record, it is retired and the backup moved aside
without requiring its digest. A record the health commit can still prove
is left to it.
The existing current-process handoff test wrote metadata ReadPendingUpdate
rejects, so it passed without reaching the PID check; it now records a
valid handoff.
Fixes#10732
A push to main-v2 no longer redeploys the site; the release workflows
already dispatch pages.yml when a release needs it, and anything else is a
deliberate run.
A push to main-v2 no longer deploys the accounts, crash-report or forum
Worker. The maintenance line takes community fixes in batches, and a merge
that touches workers/ must not reach production as a side effect; a deploy
is now a deliberate workflow_dispatch.
#10084 sends reasoning_content on every assistant turn under the deepseek
protocol in thinking mode, because the API rejects a turn without the key
and accepts an empty one. The stale-history repair retry therefore carries
the key emptied rather than absent; asserting absence asked the retry to
reproduce the 400 it exists to recover from.
#10379 clips every menu row to the terminal width, but reads that width
with term.GetSize on stdin's fd. On Windows the console input handle has
no screen buffer, so GetSize fails with "The handle is invalid", the
width falls back to 80, and a 50-column console still soft-wraps the
header and stacks a copy of it on every keypress.
termSize now reads os.Stdout's fd, which the menu writes to anyway, in
both selectOne and selectMany. Raw mode still applies to stdin.
Verified in a 50x24 ConPTY running `reasonix setup`: the header is
clipped with an ellipsis and the frame redraws in place.
The PR raised boot.go's repolint budgets to fit the shared-watcher
wiring. Choosing between the caller's host-lifetime service and one this
build owns now lives in buildSkillWatchService in skill_watch.go, so
build() is shorter than before the PR; the boot.go budgets are restored
and function-size lowered to what build uses.
closeSharedSkillWatchService used to clear App.skillWatch, so a
controller build racing shutdown would create a fresh service, and a
helper process, that no later step closes. The closed service now stays
in place; a closed service hands out dead subscriptions, so such a
build keeps its initial scan and spawns nothing. A test pins that a
build after close gets the same closed service.
#9464 treated a request_url equal to base_url as unset, but compared the
two as strings in three slightly different ways. An override differing
from base_url only in scheme or host case still POSTed to the API root,
and the legacy OpenAI chat_url, which editors mirror the same way, was
not covered at all.
provider.EndpointOverrideRepeatsBase now decides this once, following
the stricter comparison from #10658: scheme and host compare
case-insensitively, the path compares exactly apart from a trailing
slash, and an override carrying a query or fragment is deliberate and
always stays. The openai provider applies it to request_url and chat_url,
the anthropic provider to its root with and without /v1, and the
responses provider to its base URL.
Endpoint resolution moves out of anthropic.New and responses.New into
endpoint.go in each package, which also brings both files back inside
their repolint budgets. Tests cover the case, query, fragment and
chat_url cases per provider and the comparison itself.
#8870 named the non-streaming response in the message but still wrapped
io.ErrUnexpectedEOF, so IsConnReset matched it, the stream was wrapped as
an interruption, and the transcript hint told the user the provider or
proxy dropped the connection — the opposite of the new diagnostic.
The error now wraps provider.ErrNonStreamingResponse instead. IsConnReset
refuses it, so the attempt is not replayed as a transport cut, and
explainError reads the sentinel with errors.Is to show a dedicated hint
(en, zh, zh-TW) pointing at request_url/base_url. Tests assert the
identity on the stream error and the hint chosen for it.
The diagnostic moves into nonstream.go as an sseProbe so readStream does
not grow, the thought-signature fallback becomes a chatToolCall method,
and the PR's tests move to nonstream_test.go, keeping openai.go and its
test file inside their repolint budgets.
CompactionDone cleared the tab's activity status for every trigger. A
manual /compact runs outside a turn, so nothing else clears it and the
reset is right; pressure, overflow and tool compactions happen inside a
running turn, and clearing there briefly marks a working tab idle.
Non-manual CompactionDone keeps the tab thinking, as before #9231, and a
test pins the automatic triggers.
Merged together, three verified community fixes pushed files past their
repolint size budgets. Each block moves out whole; nothing is compressed
and no baseline changes.
- chat_tui_test.go: #7768's working-line wrap tests move to
chat_tui_working_line_test.go.
- math-golden.test.ts: the Young-diagram cases move to
math-young-diagrams.test.ts. The runner discovers it, and 242 + 28 cases
pass, the 270 the file held before.
- RemoteConnectWizard.tsx: formatBytes and parentOf move to
remoteWizardFormat.ts. Typecheck and the wizard suites (88 and 28) pass.
setupUsage was inserted between resolveSetupTargets' doc comment and its
func, so the comment attached to setupUsage instead. setupUsage now sits
above the comment; no behaviour change.
The PR dropped "strings" from bootstrap/launch.go when shellQuote moved
to remote.ShellQuote, but launch.go uses strings elsewhere on this
branch, so the merged tree did not compile. The import is restored.
The PR also reflowed the passphrase-KDF comment in client_test.go with
no change in meaning; that hunk is reverted to keep the diff to the fix.
internal/fileref is a utility-layer package and may not import
internal/textutil, so the comparator moves into fileref itself; desktop,
cli and control already import fileref, and there is one copy.
internal/cli/complete.go went over its size budget with the new sort
closure. The @-reference completion block (activeAtToken, file and MCP
resource items, path splitting) moves to complete_at.go, and the file's
file-size budget is lowered to what it now uses.
GoReleaser renders the whole cask, custom_block included, as a Go
template, so the bare {{staged_path}} that Homebrew's postflight_steps DSL
expects was parsed as a GoReleaser function and the release failed with
'function "staged_path" not defined'. Emit it as the string literal
{{ "{{staged_path}}" }}, the same escape GoReleaser uses for desc. With
goreleaser v2.18.2 a snapshot release now succeeds and the rendered cask
carries run "/usr/bin/xattr", args: [..., "{{staged_path}}/reasonix"].
The comments added with the open_id-first ordering narrated the incident
in Chinese, and the one in feishu.go pushed the file past its size budget.
The code comment is removed, since even one line keeps the file over its
budget; the constraint (allowlists record open_id, and card callbacks must
resolve like handleSDKMessage) now lives in English on the regression test
that pins the order. No behaviour change.
The comment explaining why registry presence only suppresses per-tool
duplicates ran to five lines, over the three-line comment budget. It now
states the same constraint in three. No behaviour change.
The normalization helpers and their two tests pushed usecapability.go and
usecapability_test.go past their file-size budgets. They move unchanged
into usecapability_skillargs.go and usecapability_skillargs_test.go, one
concern per file. No behaviour change.
The comment on managedConfig grew by a line to describe the new rule, and
the PR raised the repolint baseline (essay, file-size, function-size for
boot.go and the tree totals) to make room for it. The baseline is not
widened to land a change, so restore it and state the same rule in the
original four lines. The fail-closed behaviour and its tests are unchanged.
#8365 drops effort from the provider-wide projection so a level /effort
wrote to the canonical member no longer blocks canonicalization. The same
projection also compares two legacy members with each other, and there
dropping effort let deepseek-flash and deepseek-pro with different levels
merge silently: officialProviderFromLegacy starts from the first member,
so the other member's level was discarded.
Legacy members are now compared on effort directly, so differing levels
keep the family uncanonicalized exactly as before #8365, and each member
keeps what the user chose. Canonical-vs-legacy comparison still ignores
effort. The projection comment is cut to three lines and the regression
test's doc states the constraint rather than the incident.
RemovePluginModelRefs pushed edit.go past its file-size budget and its doc
comment, like the two in repairPluginModelRefs, ran past the comment budget.
The method moves unchanged into plugin_model_refs.go and the three comments
are cut to state the constraint only. No behaviour change.
The Studio line now owns the CLI install channels: npm `reasonix` with its
`@reasonix/cli-*` platform packages, the esengine/homebrew-reasonix cask, and
the R2 CLI update pointers (`cli/{stable,preview}/latest.json`) that
`reasonix upgrade` and the site read through the release gateway. A 1.x
release must never write any of them again.
- release-npm.yml is removed, and release-promote/release-stable no longer
call it or wait on it; the legacy recovery input publish_npm goes with it.
- release.yml no longer hands GoReleaser a tap token, no longer pushes the
prepared cask, no longer advances the CLI channel pointers, and no longer
requires npm `latest` to have moved. It still creates the vX.Y.Z GitHub
release, its notes, the immutable cli/releases/<tag>/latest.json record,
and the Desktop compatibility asset.
- .goreleaser.yaml drops homebrew_casks.
- verify-stable-release-artifacts.sh and the publication ledger stop
requiring npm package identities, npm dist-tags, and the cask version.
- Publisher helpers with no remaining caller are removed:
publish-homebrew-cask, finalize-npm-official-release, resolve-npm-release,
npm/publish-candidate, decide-cli-pointer-update, compare-cli-release-tags.
- release-workflows.test.sh now fails if any workflow holds NPM_TOKEN or
HOMEBREW_TAP_TOKEN, names the tap, runs npm publish or dist-tag, or
writes a CLI channel pointer.
Candidate preparation, tags (including npm-vX.Y.Z), Desktop publication, the
Stable manifest, Pages, and the release-event record are unchanged.
DeepSeek's pricing page defines peak as weekday windows "excluding
Chinese public holidays", but DeepSeekRateBand only knew weekends, so
holiday weekdays (e.g. National Day Oct 1) kept billing at peak rates
(#10724).
Add the 2026 statutory-holiday date table from the State Council's
notice (evaluated on the Beijing-local date) ahead of the window check.
Make-up workdays stay off-peak: they are calendar weekends and the same
page keeps weekends off-peak in full. The table needs a yearly extension
when the next notice publishes (~Nov 2026 for 2027); noted in a comment.
bypassed() matched only exact entries and a leading "*". The bypass list
Clash, v2rayN and xray write by default is `<local>;localhost;127.*;10.*;
...;192.168.*`, so every trailing-wildcard entry missed. It also lacked
WinINET's implicit rule that loopback goes direct unless `<-loopback>` is
listed.
In the default auto mode, on Windows with such a system proxy, requests
to local and LAN model endpoints went through the proxy: Ollama or LM
Studio on 127.0.0.1, vLLM on 192.168.x. That added a hop, failed outright
when the proxy routed to a remote exit, and made a cancelled stream keep
the local model generating. Cancel closed only the client-to-proxy hop,
and the proxy held its upstream connection open.
This is why TestStreamCancelDoesNotReconnect hung on a developer machine
with a system proxy. CI's Windows runners have no system proxy, and the
package is not in the PR smoke set.
"*" is now a wildcard anywhere in an entry. Only "*" is special, and
brackets are trimmed from IPv6 entries. localhost, 127/8 and ::1 bypass
unless `<-loopback>` is present. TestBypassed gains the trailing-wildcard,
near-miss (192.169.* vs 192.168.*, 110.* vs 10.*), mid-pattern and
loopback cases. It fails on 8 of them without the fix.
Two weaknesses found while triaging the open CodeQL alerts on main-v2,
none of which they flagged:
- POST /submit {"input":"/migrate --from <dir>"} read and imported
sessions from any host directory the client named, and told it whether
the path existed. The HTTP entry now refuses a /migrate or /migration
that names a source, next to its refusal of `!` shell input. A plain
/migrate still works there; the terminal UI and desktop keep --from.
- The Weixin bot saved its token under the ilink_bot_id from the upstream
login response. A malicious or intercepted upstream could place the file
outside the accounts directory. The id must now be a single local file
name.
Each has a test that fails without its fix.
The 196 open alerts themselves are false positives: session ids are
validated by SessionRef.validate, legacy transcript paths are confined by
resolveSessionPath, the model-settings probe is pinned to loopback, and
proc.Command runs argv without a shell.
Dependabot flags the three Cloudflare Workers:
- hono <4.13.5 (medium, three advisories): a runtime dependency of every
deployed worker.
- sharp <0.35.4 (high): dev-only, reached through wrangler's miniflare.
- vitest / @vitest/mocker <4.1.11 (medium): dev-only.
The package.json ranges already allowed the fixed versions; the lockfiles
held the old ones. hono moves to 4.13.9 and wrangler to 4.139.0, which
brings sharp 0.35.4. pnpm/npm audit reports no known vulnerabilities for
accounts, forum and crash-report, and each passes typecheck, its vitest
suite and `wrangler deploy --dry-run`.
The capability-route block instructed the model to call connect_tool_source
for the skills source, but that tool is retired and registered nowhere. In
production the routing catalog was built from ToolContractEntries()
(provider-visible only), so SkillEntries always saw skillToolReady == false
and marked every skill StatusConfigured with ConnectSource "skills",
yielding "source:skills ... first call connect_tool_source with source=skills".
Build the catalog from AllToolContractEntries() so hidden optional tools
(run_skill/read_skill/read_only_skill) count toward readiness, and make
RenderTransientBlock treat ClosedLoop||CapabilityProxy as "the proxy is the
only connector": never rewrite to source: or emit connect_tool_source, and
route a configured skill through use_capability with its concrete id.
confineWrite routed a managed config file (config.toml, legacy v0.x
config.json) to the per-write fresh human approval only when the target sat
outside the write roots; a root widened to cover the Reasonix home made the
same files plain allowed writes, silently editable in YOLO. Those files
configure providers, sandbox rules, and permissions for future sessions, so
no posture and no widened root may write them unattended: check
managed.Match(target) before root confinement and always consult the
approver. The fail-closed message no longer points at allow_write, which no
longer authorizes config edits.
DeepSeek rejects an assistant history turn in thinking mode when the
reasoning_content key is absent, plain text turns included. The builder
gated the key on hasReasoningOrToolCall, so a plain turn with no
reasoning and no tool call serialized without it and the request 400'd
("The reasoning_content in the thinking mode must be passed back to the
API").
Emit the key on every assistant turn under the deepseek reasoning
protocol in thinking mode, an empty string included. The contract hinges
on reasoning_protocol = "deepseek", not generic thinking: other
protocols (GLM, Kimi K3, generic thinking) keep the narrower tool-call
guard, and thinking-disabled DeepSeek keeps its existing guard.
The ask-tool chooser renders len(options) real options followed by the
'Type something' and 'Chat about this' rows, and numbers every row — but the
number-key handler only mapped digits onto real options, so the last two
rows were unreachable by digit and silently ignored. Extend the mapping to
rowCount() so a digit on those rows does what Enter does: open free-text
entry, or dismiss the prompt to chat.
* docs(release): prepare v1.39.0 notes
Summary:
Generate a bilingual, product-focused draft from merged pull request metadata. Reuse the selected release-bound PR when one is available.
Verification:
Validate the catalog, citations, bilingual fields, and rendered GitHub release notes before committing.
* docs(release): clarify v1.39.0 provider failure behavior
Problem: The generated notes imply every provider failure returns immediately, but semantic protocol repair may still make a bounded follow-up request.
Root cause: The draft described HTTP retry removal too broadly.
Fix: Scope the claim to ordinary HTTP and network failures in both languages.
Verification: Release catalog validation and all release-notes tests pass.
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: SivanCola <[email protected]>
The macOS tray template-ized the full-canvas color appicon, so the menu
bar showed a solid black rounded block instead of the logo (#10715).
- Add build/trayTemplate.png + @2x: black/alpha-only silhouette of the
logo path from build/appicon.svg, authored at 18px/36px with even
padding; macOS paints template images from the alpha channel.
- icons.ts: darwin tray candidates prefer the template asset.
- tray.ts: recognize macOS template naming ([email protected]), attach the
@2x sibling as a retina representation, and never setTemplateImage a
non-template fallback (small color icon instead of a black block).
- packaging: stage the template pair into resources/icons and require
them in the artifact member manifests.
Problem: invalid historical tool arguments could permanently block newly created or resumed conversations at the transcript gate. Missing historical images and optional invalid extension replacements could similarly prevent later requests.
Root cause: runtime rebuilds consumed provider projections that had discarded host-recorded refusal evidence. Recovery and extension validation occurred too late or used cached failures.
Fix: preserve refusal evidence by stable result identity; archive unreplayable calls as quoted historical observations; recover unavailable past image inputs; validate optional extension replacements before adoption; preserve the latest authored request when abbreviating oversized tool results; and provide localized recovery guidance. Canonical session history and healthy provider request bytes remain intact.
CI follow-ups: make configuration-error injection portable and register catalog cleanup per App generation after its temporary directory is created, preventing open SQLite handles during Windows cleanup while preserving all behavioral assertions.
Verification: full local Go suite, go vet, full golangci-lint, focused race regressions, prompt-cache guards, and the required PR CI checks pass. Synthetic tests cover fresh-session resets, cold recovery, repeated identities, unknown execution outcomes, image loss, extension policies, protected-tail truncation, and cross-platform configuration retry. No CI gates were removed or weakened.
Problem: provider failures keep turns running through nested retries and long
waits, and raw exceptions bypass the selected Desktop UI language.
Root cause: HTTP, agent sampling and auxiliary requests own overlapping retry
loops; independent UI surfaces render backend exceptions directly. Broad
net.Error matching also misclassifies request and filesystem failures.
Fix: return transport and upstream failures directly while preserving causes,
partial output, usage accounting and bounded protocol/context corrections.
Centralize locale-aware summaries and expandable original diagnostics. Update
legacy assertions and fixture provider hierarchies without removing functional
coverage or disabling CI workflows.
Verification: root Go suite, focused Desktop checks, all 521 frontend CI
commands, production builds and budgets, lint, browser error interactions, and
the latest PR CI including cross-platform, race and memory checks passed.
This merges the fix into main-v2; it does not publish a release.
Problem: Purged archived sessions could reappear as legacy filename rows, and late runtime snapshots could republish removed tabs.
Root cause: Head observations were cached against the transcript checkpoint instead of the independently published index. Missing indices manufactured path-only identities, lazy pages and lifecycle receipts used different source keys, and binding validation ended before serialized runtime publication.
Fix: Validate cached head indices against their own file and log, suppress unidentified rows for mapped sources, use the head-aware adapter when flat identity is ambiguous, include lazy source aliases in receipts, and revalidate bindings at publication.
Verification: Focused archive/purge/fork and late-publication regressions, Desktop Go race tests and broad lifecycle tests, Desktop build, golangci-lint, repolint, and documentation-impact check.
Problem: Long session names consumed the titlebar, and renaming a mock session could change the project folder or leave the visible title stale.
Root cause: Unsaved local tabs omitted their topic identity from the rename selector; the mock resolver accepted an empty target and the mock rename did not synchronize tab metadata or notify tree subscribers.
Fix: Cap displayed title width, require a concrete session target, pass the topic identity, and update the mock tree and tab title together for manual and AI renames.
Verification: Project/topic lifecycle regression, frontend typecheck, ESLint, CSS syntax check, and browser rename interaction.
* fix(transcript): reconcile queued guidance by message identity
Problem: A queued guidance insertion could appear twice when its live event and durable history row reached the Desktop in different orders. Unapplied guidance warnings had the same duplicate-row failure.
Root cause: Live receipts and canonical records used unrelated display identities, while some legacy history rows omitted the owning message ID.
Fix: Assign one message ID to each guidance insertion, retain legacy record addresses, normalize older history identities, and merge event and canonical rows by identity. Preserve inbox consumption receipts independently of display and cover applied and unapplied guidance in local, remote, and browser paths.
Verification: Focused Go and frontend regressions, root Go suite, frontend transcript suite, Chromium DOM transcript workload, typecheck, and repolint passed.
* test(frontend): typecheck steer transcript regressions
Problem: The new transcript regressions executed successfully but their fixture types failed the repository test-typecheck gate.
Root cause: Inferred literal state types were narrower than reducer results, while broad item and history interfaces hid the notice-specific fields required by assertions.
Fix: Type the reducer state at its public boundary and narrow notice items explicitly without changing the behavioral assertions.
Verification: pnpm test:typecheck, canonical user display test, and steer transcript test passed.
A thinking model can finish a turn with non-empty reasoning and an empty
visible message. That shape is accepted on purpose, and the transcript renderer
handles it: a "thinking" marker always, the reasoning itself under
--show-thinking.
The headless sinks have neither. `runOutputSink.Emit` kept only `e.Text`, so a
reasoning-only turn left `-p` printing nothing at all and `--output-format json`
reporting `result: ""` beside `subtype: "success"`, `is_error: false` and exit
0. A caller cannot tell that apart from a turn that produced nothing, and the
run looks successful either way.
Measured against a local provider: the turn's whole output was a malformed tool
call the model wrote as prose in the reasoning channel instead of emitting as a
tool call. 84 characters, the only thing the turn produced, dropped on the
floor - the automation contract said success with an empty result.
`result` now falls back to the turn's reasoning when the visible message is
empty, which is the remedy this repository already applies elsewhere for the
same provider shape. `result_from_reasoning` reports the substitution, so the
reasoning is not passed off as visible text the model never emitted; it is
additive and omitted whenever the model answered normally.
The answer and its provenance are one value rather than two fields, because
they are only ever correct together: a stale fromReasoning beside a fresh
answer would misreport where a later visible answer came from. repolint asked
for the same grouping independently, a thirteenth scalar on runOutputSink being
over its ceiling.
Nothing here reclassifies a reasoning-only turn as an error - that shape stays
accepted, and the Plan/Goal side of it is #8023's. --show-thinking is not the
lever either: buildRunSink never passes it to this sink, so gating the fallback
on it would leave `-p` printing nothing exactly as before.
Problem: v1.38.12 candidate preparation is blocked by Cloudflare challenges against curl, even with the updater User-Agent.
Root cause: Cloudflare scores the transport as well as the header; protected Actions probes repeatedly returned HTTP 200 for the shipped Go updater client and 403 for curl/Node profiles.
Fix: Reuse the Go updater HTTP request identity for every Stable manifest gate and postflight, with explicit Go setup, bounded response size, HTTP/challenge rejection, and JSON version validation.
Verification: three protected Go probes returned HTTP 200 for v1.38.12; focused Go and shell fixtures, release control contracts, release workflow contracts, and local public preflight pass.
Problem: Candidate preflight still receives a Cloudflare challenge after adding the updater User-Agent to curl, while Node fetch with an updater User-Agent succeeds.
Root cause: User-Agent alone does not establish whether the shipped Go HTTP client can reach the public endpoint; client transport behavior may also matter.
Fix: Add read-only protected probes for the current and target Stable updater using Go net/http and Node fetch. Do not expose the Cloudflare token to the Go probe.
Verification: Go probes return HTTP 200 locally; gofmt, node --check, and git diff --check pass.
Problem: GitHub runners receive Cloudflare challenge 403 for the public Stable manifest when the release gate uses curl default headers, blocking candidate preparation although the Desktop updater can read the same endpoint.
Root cause: Public-access checks and postflight used a generic request identity that Cloudflare classifies differently from the shipped updater.
Fix: Centralize manifest requests through a versioned Stable updater User-Agent across candidate, publication, site recovery, and public verification. Keep HTTP and JSON validation fail-closed.
Verification: release control contracts and site ownership fixtures pass; three protected Actions diagnostics showed default 403 and updater 200.
* fix(release): inspect Cloudflare config rules for download access
Problem: GitHub runners receive a Cloudflare challenge for the Stable manifest, and the existing diagnostic does not inspect path-scoped configuration rules.
Root cause: The read-only diagnostic omits the http_config_settings phase, so it cannot report whether Browser Integrity Check is already overridden for the download path.
Fix: Include the configuration settings phase in the protected ruleset inspection.
Verification: git diff --check and node --check scripts/inspect-cloudflare-download-access.mjs.
* fix(release): probe the actual updater request profile
Problem: The public manifest diagnostic uses only Node default headers, while the Desktop updater sends a distinct User-Agent to avoid edge bot challenges.
Root cause: A 403 for the diagnostic client alone cannot distinguish runner-wide blocking from User-Agent scoring.
Fix: Report a second read-only probe using the current Stable updater request profile.
Verification: git diff --check and node --check scripts/inspect-cloudflare-download-access.mjs.
Problem: the protected release candidate is blocked by a Cloudflare challenge, while the existing Actions diagnostic collapses GraphQL failures into a generic error. Root cause: the query includes fields and filters without a verified schema contract and the diagnostic hides whether failure is schema or permission. Fix: query documented fields over a short window, filter only the public manifest in memory, and report allowlisted error classes without visitor or credential data. Verification: node syntax check, repository lint, and staged diff check passed.
* docs(release): prepare v1.38.12 notes
Summary:
Generate a bilingual, product-focused draft from merged pull request metadata. Reuse the selected release-bound PR when one is available.
Verification:
Validate the catalog, citations, bilingual fields, and rendered GitHub release notes before committing.
* docs(release): review complete v1.38.12 release scope
Review the full first-parent range from v1.38.11, including direct sidebar merges, model catalog updates, Windows compatibility, browser runtime changes, and manual compaction behavior. Preserve existing model defaults and document the cache effect of explicit compaction.
Verification: release catalog validation, release-notes tests, guide target checks, privacy review, and git diff --check passed.
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: SivanCola <[email protected]>
Problem: full main-v2 push tests and coverage failed after the sidebar repair because the generated Desktop migration inventory was stale. Root cause: workspace-removal extraction shifted source locations and GetProjectTreeSnapshot gained an error result. Fix: regenerate the Markdown and JSON inventory without changing runtime code or weakening the gate. Verification: inventory tests, repository lint, PR CI, and diff checks passed.
Problem: the main-v2 push CI failed root tests and coverage because the generated Desktop migration inventory was stale after the sidebar repair merged.
Root cause: the inventory records source locations and Desktop RPC signatures; extracting workspace removal moved methods, and GetProjectTreeSnapshot now reports read errors.
Fix: regenerate both inventory artifacts from the current main-v2 source without changing runtime code or weakening the inventory gate.
Verification: go test ./tools/desktopinventory, go run ./tools/repolint, and git diff --check passed.
Problem: project folders and conversations could disappear, duplicate, or fail canonical navigation after workspace removal. Root cause: workspace and session identity transitions were not reconciled consistently across backend and mounted sidebar. Fix: serialize and reuse workspace conversations, restore explicit visibility, fence stale sidebar reads, and preserve native-history navigation. Verification: full PR CI passed across supported runners, plus focused race, frontend, and native Windows ARM64 package checks.
Problem: recent historical-session and creation changes advanced main-v2 while this sidebar fix was in flight, and the removal implementation exceeded repository complexity budgets.
Root cause: the generated Desktop contract contained independent additions, and removal snapshot, durable bookkeeping, and runtime unlinking lived in one oversized function.
Fix: regenerate the combined contract; keep both upstream changes; extract removal phases into owning helpers and move focused regressions into dedicated test files.
Verification: focused Desktop workspace/navigation tests, frontend sidebar tests, production frontend checks, repolint, and diff checks passed on the integrated tree.
Problem: a re-added folder was visible in the registry snapshot but remained absent from an already-mounted Windows sidebar.
Root cause: selecting an existing topic after restoring workspace visibility emitted no project-tree change notification.
Fix: publish the completed visibility change after successful workspace activation.
Verification: failing notification regression is green; Windows package reproduction established the mounted renderer symptom.
Problem: folder addition could disappear, reopen could allocate extra conversations, and removal failures could leave visibility and runtime bindings inconsistent.
Root cause: workspace selection created topics unconditionally, refresh retries repeated successful opens, unordered shell reads mixed registry membership with catalog revisions, and removal crossed persistence and runtime boundaries without admission fencing.
Fix: reuse existing conversations under serialized navigation; restore explicit re-add visibility; reserve removal and roll back failed bookkeeping; retain completed blank-project opens across refresh retry; reject stale shell responses using a separate workspace generation and fail registry reads explicitly.
Verification: repeated/concurrent/restart opens, removal rollback and admission, mounted shell ordering, registry integrity, project-tree and canonical navigation regressions, focused race tests, frontend typecheck/build and sidebar browser regression passed.
Problem: a placeholder and its live or canonical projection could appear as two new conversations.
Root cause: projection ownership was missing for pathless tabs and first durable sessions.
Fix: publish exact placeholder aliases and cache workspace alias maps; keep canonical branches with shared topics distinct.
Verification: placeholder identity, branch ambiguity, runtime projection and organization compatibility regressions passed.
Problem: canonical navigation from native history reported an unavailable identity protocol, including after workspace removal.
Root cause: the source runtime was required to carry a reusable canonical binding.
Fix: route supported native controllers through prepare-and-swap while retaining strict checks for unsupported runtimes.
Verification: real-controller removal, no-removal and cancelled replacement tests; canonical navigation and focused race tests passed.
Problem: New Session could fail after workspace identity reconciliation, delay access to the new input, and display unrelated creation failures.
Root cause: Creation compared journal workspace IDs outside the registry transaction; navigation waited for runtime readiness; global recovery duplicated the active request observer.
Fix: Resolve directory ownership at reservation and publication, preserve retry identities, expose the persisted input before runtime readiness, and scope recovery feedback to the selected composer. Keep send readiness, input persistence, lifecycle protection, and provider-visible contracts intact.
Verification: Creation and workspace interleaving regressions; focused Go race tests; frontend lifecycle and browser scenarios; production build and bundle budgets; native Windows portable-package creation/restart checks; PR cross-platform CI.
* Fix historical session reconciliation and ordinary lifecycle
Problem: Historical conversations could remain visible but fail to open or archive after upgrades.
Root cause: Strict receipt ownership checks had no ordinary discovery reconciliation, and navigation mislabeled underlying failures as missing projects.
Fix: Recover proven mappings and absent targets through lifecycle-fenced journals, retain authoritative adoption and tombstones, filter unusable rows, and preserve classified errors.
Verification: Catalog and workspace-state tests; desktop E-H, Q-S and history-10-11 partitions; focused race and restart tests; frontend navigation, typecheck, browser loading and production build; Windows cross-compilation.
* Index completed historical migration receipts during discovery
Problem: Ordinary history discovery repeatedly scanned every migration receipt for each legacy head, causing quadratic metadata lookup cost.
Fix: Build a completed-receipt key index once per discovery pass while preserving reviewed-version and interrupted-operation completion evidence.
Verification: Focused historical discovery, visibility, and receipt-index tests pass; desktop golangci-lint and repository lint pass.
* Use readable sessions in the read-only sidebar fixture
Problem: The project-tree read-only test attached eight registry identities without session files and expected them to be visible, contradicting the new unavailable-history contract.
Fix: Create real sessions before attaching them while preserving the unmigrated organization and byte-for-byte registry immutability assertions. Separate coverage continues to require missing members to stay hidden.
Verification: Reproduced the original assertion locally; focused read-only, organization, and missing-member regressions pass with race detection. Pinned desktop lint and repolint pass.
Problem: Transcript streaming could rerender unchanged tool rows, reasoning, and file cards, contributing to renderer long tasks. The full CI suite also contained a fork assertion tied to obsolete prop plumbing and a native browser smoke fixture that released its observation lease before input.
Root cause: Rows observed full process summaries, fork state flowed through every row, and reasoning projections retained unrelated answer fields. The auxiliary checks modeled implementation details or a shorter observation lifetime than production.
Fix: Subscribe rows to primitive visibility, scope fork state to tail controls, isolate reasoning-owned fields, and memoize file cards. Cover unchanged renders and positive interactions; align the fork assertion and native browser smoke lifecycle with their product contracts.
Verification: Local frontend transcript suite, all 463 frontend suites, Electron shell typecheck, 307 shell unit tests, native macOS browser smoke, production frontend build and bundle budgets, and repository lint passed. PR #10691 CI passed across frontend, Linux browser transcript, memory, CodeQL, Desktop Go/race, macOS, and Windows jobs.
* fix(model settings): preserve session processes across runtime rebuilds
Problem: saving model settings could strand a Desktop session when an independent background gateway kept a job registered, repeatedly rejecting new messages.
Root cause: rebuild admission treated every background job as runtime-bound, while replacing a controller would cancel its job manager and kill independent processes.
Fix: move background process ownership to a session scope, classify runtime-bound work explicitly, publish rebuilt controllers atomically, and carry structured application status and one-submit validated applied-settings recovery through local and remote Desktop paths.
Verification: root Go suite, inventory-partitioned Desktop Go suite, focused race suites, pinned lint, repolint, frontend typecheck/build/tests, browser interaction and Electron smoke passed. Windows shell tests cross-compiled.
* fix(desktop): keep remote sends and CI component loading compatible
Problem: the frontend CI component runner could not load the recovery CSS import, and a remote submission guard rejected different messages sent in the same tick.
Root cause: the component imported CSS in Node test scope, while the guard treated every in-flight send as an unresolved receipt.
Fix: include recovery styles through the existing global stylesheet, deduplicate matching in-flight sends, and reserve receipt reuse/blocking for genuinely unknown outcomes.
Verification: the failed decision-slots test, remote-session-surface test with same-tick and unknown-result assertions, browser recovery interaction, CSS syntax, production frontend build, TypeScript and repolint passed.
* test(frontend): await both file-reference batches before cache assertion
Problem: the full frontend CI runner could fail the cached-verdict assertion before the second host batch had settled.
Root cause: the test assumed six Promise continuations always covered two async batch applications; that scheduling count varies by runtime.
Fix: yield through the host batch completion turn before asserting the unchanged per-candidate cache invariant.
Verification: the exact file-reference test passed, and the complete inventory-verified frontend CI runner passed all 516 commands locally.
* test(desktop): await catalog fixture shutdown on Windows
Problem: The retired projection test can remove a temporary SQLite database while its asynchronous shutdown still holds a Windows file handle.\nRoot cause: stopSessionCatalog has a bounded deadline and may return before Catalog.Close finishes.\nFix: Join the catalog close before advancing and register a cleanup fallback for early test failures.\nVerification: go test -run '^TestMissingProjectDoesNotResurrectDeletedTopic$' -count=1 . (desktop)
* chore(desktop): regenerate inventory after base merge
Problem: The merged historical-session changes shifted Desktop source locations, leaving the checked-in migration inventory stale.\nRoot cause: The inventory records line hints, and the upstream changes were merged after the prior generation.\nFix: Regenerate both inventory artifacts with the repository tool.\nVerification: go test ./tools/desktopinventory; go run ./tools/desktopinventory -check.
Problem: Session switching and removal can hang during preview operations,
AI renaming, or autosave; cancellation can also leak a browser page.
Root cause: Preview cleanup reenters App locking, removal reverses title
lock ordering, and runtime teardown joins autosave under locks it needs.
Fix: Separate preview publication from lifecycle revocation, standardize
removal lock ordering, defer joins outside title/index locks, and reclaim
cancelled agent-owned browser pages while preserving user takeover.
Verification: Deterministic lifecycle regressions, focused Go race tests,
broader lifecycle suites, 307 Electron tests, typecheck, build, repolint,
and pinned Desktop golangci-lint.
* fix(desktop): reconcile retained historical archives
Problem: retained converted sessions could fail archive, remain as ghost rows, or collide with deleted target identities.
Root cause: source receipts and pending imports did not distinguish identical retired copies from later source history, and archive could publish an active import before committing its lifecycle.
Fix: prove exact conversion ownership and content, repair identical retired receipts, stage changed history directly under an archive transaction, and fence recovery and sibling session operations.
Verification: desktop partitioned regression suite, focused archive and damaged-history tests, workspacestate tests, frontend typecheck and tests, repolint, and Windows amd64 cross-compile.
* fix(desktop): handle historical proof service shutdown errors
Problem: Windows desktop lint rejected unchecked shutdown results for the short-lived historical proof services.
Root cause: newly added read-only proof paths deferred Shutdown without handling its error.
Fix: close proof services through a shared helper that records cleanup errors in the local service log.
Verification: Windows-target golangci-lint v2.12.2 with isolated cache, archive regression tests, repolint, and gofmt.
* fix(desktop): retire archived list reads and quiet background refreshes
Problem: Archiving during a pending sidebar read could strand a four-row
list in loading forever, and routine background reads showed loading on
each conversation update.
Root cause: Session archive advanced request generations without retiring
pending/page state. Foreground and background reads also shared one visible
loading indicator despite an already resident page.
Fix: Use the shared list invalidation owner for both archive paths. Keep
request ownership separate from background loading presentation, retaining
resident rows, expanded windows, pagination controls, and reading anchors.
Verification: All 20 project-tree suites, frontend typecheck, targeted
ESLint, repository lint, and diff checks passed. Browser checks confirmed
quiet refresh, stable reading position after insertion, and foreground
pagination feedback with no console errors.
* test(desktop): align topic removal fixture with list invalidation owner
Problem: The broader frontend CI suite failed in the topic-removal
controller fixture after the archive controller dependency changed.
Root cause: This sibling fixture still passed the old raw page refs instead
of the shared invalidateProjectTopicLists callback.
Fix: Type the fixture against the controller parameters, provide a real
project identity and the new callback, and assert only committed removals
invalidate their owning list, including a successful idempotent retry.
Verification: The previously failing topic-removal-controller suite,
targeted ESLint, repository lint, and diff checks pass. The full frontend
CI test entry point is also running locally.
* test(desktop): cover exhausted sidebar lists after archive and refresh
Problem: An exhausted folder can appear to keep loading even when only one
session remains, so large-list coverage does not directly express this case.
Root cause: Existing regressions covered an empty background refresh and a
multi-page archive race but not their one-row and terminal-page boundaries.
Fix: Exercise zero and one resident row, plus archive completion with twelve,
two, and one initial rows under old-first, fresh-first, and obsolete-error
ordering. Verify terminal pages expose no next-page control, finish loading,
and make no additional requests across six minutes of simulated time.
Verification: project-tree-loading.test.tsx, targeted ESLint, repository lint,
and git diff --check passed. No production behavior is changed by this commit.
* fix(desktop): recover interrupted manual session creation
Problem: pending manual creations can remain starting after a competing
process releases its creation lock.
Root cause: startup recovery was a one-shot attempt and build notification
completion did not establish that an executor had actually stopped.
Fix: centralize creation admission and retries, preserve OS-lock ownership
through execution and result persistence, join actual build executions on
shutdown, and expose transient progress plus bounded diagnostics. Preserve
session identity, persisted schema, and unknown payload fields.
Verification: native macOS subprocess handoff/exclusion, targeted Go race
and shutdown tests, frontend interaction/type checks, Chromium recovery UI,
Desktop lint, and Windows cross-compilation passed. The full Desktop sweep
retains a separately reproduced baseline activation-ordering failure.
Raise one existing test-file allowance by three measured lines to retain
pending-attachment source and target isolation assertions.
* fix(desktop): claim activation terminal state before background pruning
Problem: rapid navigation can cancel an activation that already emitted ready.
Root cause: readiness was published while the request remained eligible for
supersession until background pruning completed.
Fix: group activation bookkeeping and atomically claim the terminal event
under App.mu while retaining generation ownership for deferred pruning.
Both direct-tab and topic switches preserve cancellation of unfinished
requests without cancelling already-completed requests again.
Verification: a deterministic channel-gated regression fails on the original
base for both switch paths. Activation race tests, ten repeated fast-path
and regression runs, Desktop lint, and repository lint pass.
* chore(desktop): refresh command inventory after recovery changes
Problem: the root CI inventory freshness test rejects the new diagnostic RPC
and shifted source locations.
Root cause: the independent Desktop contract generators do not regenerate
the migration command inventory.
Fix: regenerate both inventory artifacts with the existing owner tool.
Verification: go test ./tools/desktopinventory passes; the inventory includes
ExportManualCreationDiagnostics and retains existing command classifications.
* test(desktop): own catalog fixtures before waiting for admission
Problem: Windows CLI discovery coverage could fail a three-second publication
poll and then leave its SQLite owner running during temporary-dir cleanup.
Root cause: a functional discovery test used startup speed as readiness and
registered cleanup only after that assertion succeeded.
Fix: reuse the existing initial-admission and reconcile-execution barriers;
register checked cleanup in the shared fixture before startup. Remove the
redundant completion hook and preserve topic identity, ordering, count, and
no-migration-marker assertions. No production timeout or platform is changed.
Verification: an intentionally failing subprocess proves the old helper leaks
its owner and the new helper closes it. Relevant catalog/discovery race tests
pass, as do Desktop lint and repository lint. Native Windows CI follows.
## Summary
Switching back to a running conversation could divide thousands of restored output tokens by a newly started provider timer, briefly showing an enormous rate that then decayed. Restored sessions now measure newly observed output against its matching provider-active intervals while retaining the output total separately.
The shared local/remote snapshot path resets the observation window. Live subscriptions keep the composer current, and usage settlement, cumulative tool arguments, retry backoff, reconnects, and completed-turn readings retain the same measurement boundary. A new turn returns to ordinary billed telemetry. Throughput tooltips explain restored-session sampling in all three locales.
## Verification
- Deterministic restored-turn regression across local/remote and both snapshot protocol entry points: large restored prefix, fresh deltas, CJK output, tool gaps, cumulative arguments, child isolation, usage settlement, reconnect, completion, and retry.
- Composer DOM regression: live-store updates, popover, token totals, and session isolation.
- Existing turn metrics, controller stream progress, stream batching, live context, snapshot reconciliation, conversation projection, and composer run-strip suites passed.
- Frontend `pnpm typecheck`, `pnpm test:typecheck`, and `pnpm lint:hooks` passed; root `go run ./tools/repolint` and `git diff --check` passed.
- Browser verification of the actual composer: restored 13K-token total remains visible, fresh samples show 10 then 20 t/s, and switching clears the previous rate.
Documentation-impact: none - Existing embedded documentation remains accurate; the affected throughput tooltip is updated directly in all supported locales. No new configuration or workflow is introduced.
Cache-impact: none - Renderer-only telemetry and presentation; no provider-visible prompts, tools, messages, or persisted protocol fields change.
Cache-guard: existing provider cache contracts are unchanged; the diff is confined to renderer telemetry, UI copy, and regression tests.
System-prompt-review: N/A
## Summary
Restore queue and current-turn guidance for canonical Desktop sessions, and simplify input recovery.
## Problem and root cause
Canonical sessions carry a session ID with an empty legacy path. Inbox capture, runtime selection, and receipt lookup still relied on that path, rejecting legitimate running-session messages. A registration failure could then retain a pending marker and stale revision, leaving input locked behind ineffective version choices.
## Changes
- Route capture, receipt ownership, runtime selection, and guidance through canonical session identity while preserving generation fences.
- Automatically reconcile rejected registrations and lost replies. Release only a registration whose owning renderer never invoked send; uncertain delivery is checked without replay.
- Preserve newer edits during late settlement and retain unsent input when another submission is pending.
- Replace the three version/recovery actions with automatic recovery and one Retry action when confirmation remains unavailable.
- Retire composer recovery-copy creation and display. Keep revision checks, current-input autosave, and queue-item editing conflict handling. Keep the old list RPC as an empty compatibility response; do not change the database schema or delete existing records.
- Document the simplified experience in English and Chinese.
## Verification
- Focused canonical queue/guidance, composer persistence, deterministic race, lifecycle, and follow-up tests passed; integrated-head queue, race, persistence, wiring, receipt, and stream suites passed.
- Desktop inbox and sessionui race tests passed again on the integrated head.
- Frontend source/test type checks, hook lint, app-layer checks, and repository lint passed.
- All 459 discovery-runner frontend suites passed on the final head, including the unchanged goal-recovery guard. Goal restoration remains suppressed when history changed even though retained text can be edited.
- Browser interactions verified automatic unlock after rejection, retained text, lost-response confirmation, and no duplicate enqueue on Retry.
- The runtime Chromium scenario initially failed because mock-created tabs omitted binding generations. The mock producer and runtime frames now publish matching identities; all finishing, queue receipt, reconnect, and remote ownership assertions pass locally unchanged.
- Production frontend build and bundle budgets passed. Pinned Desktop golangci-lint passed. CI supplies the remaining platform gates.
## Scope and compatibility
The removed copy/version-choice behavior is intentional. Existing persisted input and submission records remain readable. Provider stream EOF and historical transcript recovery are separate issues and are not claimed fixed here. Native Windows installer qualification and release packaging are outside this PR.
Related work: #10680 concerns manual-session creation recovery; #10657 concerns historical-version recovery. Neither implementation is adopted here.
Documentation-impact: updated - bilingual composer controls describe running-turn messages, automatic input recovery, and retired version choices.
Cache-impact: none - provider-visible prompts, tools, and message serialization are unchanged.
Cache-guard: inbox identity and receipt regressions verify recovery without replay; provider serialization is untouched.
Problem: Session changes could retain an old context estimate and overview
metrics, while failed-attempt estimates could inflate the context gauge.
Root cause: The estimate cache omitted session identity; overview reads could
re-key live telemetry; frontend budget and usage paths preferred stale snapshots
and dropped the final throttled event.
Fix: Scope estimates and overview reads to a session, preserve live context
authority, schedule a trailing usage refresh, and exclude estimated usage from
context occupancy while retaining its billable accounting.
Verification: Root Go tests, Desktop context and telemetry tests with race
detection, frontend regression tests, typechecks, repository lint, and browser
interaction checks passed.
Problem: Legacy workspace drafts confuse users, and emptying trash can add
new conversation rows that create more sessions when opened.
Root cause: The draft surface and replay lifecycle remained connected after
manual creation returned to formal sessions. Purge removed canonical topic
ownership but left metadata that looked like independent empty topics.
Fix: Disconnect draft UI, replay, and restore reads while preserving formal
input persistence and exit barriers. Keep minimal purge topic ownership,
filter stale metadata, and reject opens that would recreate deleted sessions.
Preserve formal sessions on restore conflicts and draft retirement on failed
tab-preference writes. Document compatibility and shared-topic behavior.
Verification: Frontend 457-suite runner and typechecks passed; focused Go
race suites and workspace-registry package passed; repository lint and
whitespace checks passed. Full Desktop aggregate run previously timed out;
broader cross-platform verification is delegated to PR CI.
* fix(desktop): keep purged history out of topic placeholders
Problem: Permanently deleted history could reappear as a metadata-only topic and fail subsequent archive requests with session not found.
Root cause: Sidebar adoption filtering depended on live membership and presentation, both removed by purge, while legacy metadata and source receipts remained.
Fix: Derive an immutable topic adoption index from durable committed evidence, preserve minimal topic identity in purge receipts, and share the index across materialized, lazy, and pinned projections.
Verification: Workspace state tests with race detection, archive/restore/purge/restart integration coverage, shared-topic and independent-source preservation, pinned/manual-order regressions, previous operation codec round-trip, incremental lint, and diff checks passed. Native packaged Windows/macOS UI was not revalidated.
* fix(desktop): unify migrated session identity and lifecycle
Problem: A migrated conversation could appear as both a canonical session and a historical source, highlight twice, and reappear after lifecycle operations.
Root cause: Physical path normalization changed source hashes while adoption receipts and pending imports retained older keys. Lookup and lifecycle callers did not share compatibility aliases.
Fix: Derive immutable source aliases from durable mapping receipts and use them across discovery, navigation, tab restore, import preparation, sidebar projections, and lifecycle guards. Preserve independent head and reviewed-version identities. Persist explicit source-cleanup intent with purge tombstones, clean only unchanged exclusive canonical originals under ownership locks, and retain shared, changed, offline, and multi-head sources.
Verification: Focused identity, archive/restore/purge, shared-source protection, writer contention, and process-restart tests passed. Workspace state race tests, filesystem purge tests, incremental lint, and diff checks passed. The complete Desktop CI partition run is in progress; the initial unpartitioned run hit its six-minute package timeout. Packaged Windows and macOS UI has not been revalidated.
* fix(desktop): separate migration identity and purge planning owners
Problem: The PR failed repository size and complexity checks and lacked the required documentation-impact declaration.
Root cause: Source alias lookup and purge-plan encoding grew existing lifecycle coordinators beyond their file budgets, and historical head resolution remained embedded in the generic source resolver. The historical migration guide still implied that all original files survive explicit permanent deletion.
Fix: Move source mapping lookup and versioned purge-plan construction into focused owners, extract historical head resolution, and document unified display/lifecycle and guarded source cleanup in both language guides. Preserve all behavior and test coverage without raising budgets or disabling checks.
Verification: Repository repolint, the documentation-impact validator, isolated-cache golangci-lint 2.12.2, migration identity/import/head/purge regressions, workspace-state package tests, and diff checks passed.
## Summary
Fix repeated forks from the same source conversation remaining absent from the paged sidebar until restart. Canonical fork attachment now publishes a workspace scoped catalog revision when membership becomes durable. The fork's topic presentation is written and invalidated before tab publication, so a child remains visible with its title even if opening its tab fails.
When a failed tab open is retried, the new tab reuses the durable topic ID and projected title. The legacy session directory path continues to use its catalog reconciliation flow.
The background notification browser fixture now serves its injected runtime state through focus and periodic reads as well. A forced focus read while notification loading is gated covers the test race that previously marked the snapshot failed and suppressed the expected toast.
## Root cause
The sidebar paginates durable workspace membership. Runtime projection events alone do not refresh page item keys. Separately, asynchronous topic presentation could be written after the first page reload; a failed tab open skipped the later title notification, and retry could generate a different topic ID.
## Verification
- Focused Desktop Go fork, worktree, legacy session, and catalog tests under `go test -race` passed.
- Frontend `project-tree-loading.test.tsx` passed, including two forks from the same source without remounting.
- Frontend typecheck passed for the original sidebar fix; no frontend production files changed in the follow-up commit.
- `git diff --check` passed.
- Repository lint passed. The attention notification browser test passes its forced focus ordering and all existing sound, toast, draft, focus, navigation, and deduplication assertions.
- The native macOS browser runtime smoke passed locally, including recording and 100 lifecycle cycles.
The earlier local full Desktop suite exceeded its default ten minute timeout. The final PR CI run passed on `f772a0e4a0d979e286690557735d8b4a437eb677`, including Desktop Go, all Go race and Windows Go groups, frontend/browser checks, native macOS/Windows checks, lint, and the required root checks. The previously failing notification browser group and native macOS recording/lifecycle smoke both passed. All 56 reported checks passed; no tests were removed or timeouts increased.
Documentation-impact: none - This restores existing fork visibility and title behavior without changing any documented workflow or user-facing interface.
Problem: Deleted legacy conversations could return to the sidebar after a catalog rebuild, then fail to open or archive.
Root cause: The lazy and materialized history projections ignored saved topic tombstones, and the materialized projection lost path adoption when it expanded one source into a head row.
Fix: Exclude deleted topics before ordinary paging and materialization; honor adopted path aliases for single-head sources while preserving independent branches.
Verification: focused Desktop lifecycle regressions, session catalog tests, repolint, and a 100k-session first-page benchmark.
Problem: Returning to a running conversation could place reasoning above its user message.
Root cause: The live follower sorted canonical history positions together with indexes from a separate projection. Reconnect snapshots could also select an older alias body or miscount an existing user.
Fix: Align canonical and projected rows by message identity, retain native page ordering, and preserve the authoritative active body and turn count.
Verification: Frontend and test typechecks, ESLint, transcript and Chromium browser regressions, session-loading browser tests, and PR CI across Linux, macOS, and Windows.
* Improve historical session startup and lazy loading
* Fix history preparation retirement across readers
Problem: source replacement and immediate reopening could expose a closed SQLite cache to retained history readers.
Root cause: reader cancellation did not follow preparation retirement, the last release removed cache ownership before close, and compatibility paging opened a separate owner.
Fix: share preparation across both APIs, propagate source cancellation, retain and drain cache-close barriers, and retire unbound borrowers during shutdown.
Verification: deterministic shared-reference, replacement, immediate reopen, compatibility cursor and shutdown tests passed with the related history paging race suite.
* Index history read fences and extend lazy catalog pages
Problem: paging repeatedly cloned the registry and scanned every source mapping, while existing groups, time filters and pinned shells bypassed lazy catalog reads.
Root cause: read invalidation lacked immutable reverse indexes and source fences could mix a captured WAL view with live metadata.
Fix: share exact-publication lifecycle tokens and one verification per validation; reuse lazy pages for all-session views, frozen time filters and automatic pinned shells; capture source fences from their listing transaction. Document remaining full-materialization and recovery limits.
Verification: registry, history lifecycle and catalog paging race suites; deterministic WAL-capture replacement regression; transcript tests; full root Go suite. Desktop full partition sweep remains in progress.
* Validate fresh macOS images and join history batch workers
* Page native history outlines and resolve direct read anchors
* Read schema-one history through cancellable native projections
* Handle unused history roots and verify packaged native readers
* Page grouped history without materializing every session
Problem: group filters expanded all legacy rows and loaded unrelated canonical
headers. Disappeared history roots could also be omitted from completeness.
Root cause: group membership forced the full snapshot adapter, and listing
availability independently skipped absent roots instead of using discovery.
Fix: retain immutable catalog pages with source-key membership predicates,
admit canonical group and pin members before Stat, and use metadata discovery
state for absent roots. Keep source hashes and persisted schemas unchanged.
Verification: catalog and Desktop pagination/organization regressions, related
race tests, sidebar browser interactions, and stronger packaged legacy reader
checks. Root-wide validation and the new packaged group check continue separately.
* Share native history preparation with compatibility content readers
Problem: Unbound history field requests could enter compatibility repair after
native paging had already prepared a read-only view of the same source.
Root cause: Paging and content RPCs borrowed different history preparation
owners and did not share cancellation and source validation boundaries.
Fix: Centralize native pager borrowing and use it for compatibility field
reads, including explicit targets, while retaining unsupported-format readers.
Verification: Focused paging/content regressions and race tests pass, including
Unicode chunk reads, unchanged authoritative bytes, and peer cancellation.
The native package harness now exercises unbound content reads as well.
* Bound queued history iterators and resolve original compatibility sources
Problem: Rotating discovery retained one descriptor per historical root, and
cold compatibility RPCs rejected global files after workspace restoration.
Root cause: Scan admission had no retained-iterator budget; read validation
mistook the destination workspace for the captured historical source root.
Fix: Limit queued discovery to eight iterators with a visible-workspace
reserve, retaining admitted progress. Share known-root source resolution
across cold paging, field reads, and legacy previews outside App.mu.
Verification: Catalog full race suite and Desktop focused race tests pass.
Tests cover retained progress, priority switching, eventual completion, old
global source reads, full Unicode content, and unknown/symlink root rejection.
The packaged smoke caught the original directory failure; retain compact
page diagnostics so an absent field ref identifies its actual read error.
* Filter historical metadata pages without materializing the library
Problem: Sidebar text queries bypassed lazy paging and built every legacy
session row before returning a small matching page.
Root cause: The bounded catalog adapter excluded text requests because SQL
case conversion could not preserve display-layer Unicode/title semantics.
Fix: Filter bounded catalog batches through the existing Go matching rules
inside the retained WAL snapshot. Read captured source identities instead
of resolving candidate files and account for retained query text.
Verification: Catalog and Desktop focused race tests cover sparse matches,
Unicode, cancellation, fixed-cut continuation, source keys and localized
titles. Sidebar browser interaction tests pass. Native smoke now also checks
text filtering within a group; sparse queries can still scan all metadata.
* fix(desktop): admit explicit group targets without scanning history
Problem: ordinary group edits enumerated all legacy source pages before
applying a workspace organization mutation.
Root cause: group commands shared manual reordering's full-source admission.
Fix: admit only resolved group targets inside the revision CAS, normalize
physical source keys, retain explicit ungrouping fences, and recheck source
adoption and lifecycle ownership inside the registry transaction.
Verification: organization and group regressions pass with the race detector,
including deterministic adoption-before-commit rejection and late preference
import. Document the remaining full-source manual and legacy preference paths.
* fix(desktop): decouple catalog admission from metadata refresh
Problem: catalog startup and filesystem event delivery waited for whole
project/topic metadata transactions and restored-path index writes.
Root cause: startup and the watch loop synchronously owned these writes,
while user edits started independent unjoined metadata goroutines.
Fix: register watches before restored identities settle, enqueue restored
paths, and coalesce all metadata refresh triggers into one cancellable worker
joined by the catalog lifecycle. Publish admission without waiting on writes.
Verification: catalog/sidebar regressions and race tests pass. Deterministic
coverage holds a real SQLite writer during admission, blocks refreshes to
verify coalescing, and checks cancellation and lifecycle wakeup ownership.
Whole-projection metadata reconciliation and synchronous database integrity
validation remain explicitly documented acceptance gaps.
* refactor(history): separate projection readers and preparation stages
Split native pager source validation, DAG projection, compatibility windows, catalog paging, and controller usage accounting into focused owners. Remove the unreachable continuation rebind in the empty-key topic branch. Preserve source validation, transaction boundaries, and snapshot ownership without increasing lint baselines.
Verification: repository lint; agent, sessioncatalog and session package suites; focused Desktop history/catalog/snapshot/navigation tests; frontend typecheck and transcript suite.
* fix(history): serialize discovery progress with catalog writers
Problem: the 100k native discovery fixture stalled with SQLITE_BUSY. Root cause: progress publication bypassed the process-local mutation lock while other catalog writers used it. Fix: route progress through the same boundary and preserve its generation predicate and cancellation behavior. Add a production-package scale harness and record the incomplete history.12 result without relaxing its gate.
Verification: focused metadata/reconcile/catalog race tests, repository lint, script syntax check; native scale harness smoke and history.12 measurement exposed the failure. A fresh package run is still required.
* fix(history): move metadata integrity audits behind startup admission
Problem: cached catalog opening still scanned every database page before returning a sidebar page. Fix: open advisory metadata separately from the full background audit while keeping content projection validation synchronous. Corruption revokes the generation, stops its work, closes all read leases, and reopens through the existing validated quarantine path with a monotonic revision floor. Old cursors return stale_cursor; canceled or transient audits cannot quarantine data, and retries remain bounded.
Verification: root Go suite; projectiondb and sessioncatalog race suites; Desktop catalog/history/snapshot race regressions; real CHECK-constraint corruption, source preservation, lease closure, late publication, cancellation and retry tests; repository lint. Native package performance acceptance remains pending.
* fix(history): avoid redundant discovery and source-wide metadata writes
Problem: Large legacy catalogs could finish discovery then immediately restart,
while registry refresh rewrote unrelated discovered topics.
Root cause: Startup recovery used the full-root invalidation notification
alongside watcher-owned discovery; metadata membership reset and orphan cleanup
scanned all topics.
Fix: Publish only registry metadata after reservation recovery, and use the
existing metadata_present flag with an optional partial index to constrain
membership refresh. Source mutation retains topic cleanup ownership. Record
incomplete history.13 scale evidence and warmup progress.
Verification: sessioncatalog suite, focused catalog race tests, Desktop
startup/catalog/history race tests, repolint, JavaScript syntax check, and diff
whitespace checks passed. New package scale acceptance remains required.
* fix(history): bound directory watching and merge undispatched invalidations
Problem: Large catalogs continued to restart discovery after the initial scan.
Root cause: Catalog monitoring bypassed the platform directory backend and used
macOS kqueue registration that enumerates children and opens per-file handles.
Unwatched roots were also invalidated on every metadata tick, and queued jobs
retained pre-dispatch changes as unnecessary follow-up scans.
Fix: Reuse native directory watching, preserve canonical event/access routing,
separate watch retries from the rotating audit, and merge invalidations before
scan dispatch while retaining genuine in-flight source changes.
Verification: Focused catalog queue and Desktop catalog/watcher race tests pass,
including 512-file native registration under a 256-descriptor process limit,
canonical events, recovery gaps, durable journals and late file discovery.
Repolint and diff checks pass. Record the incomplete history.14 scale evidence;
the new production package still requires measurement.
* fix(history): preserve cold reads when execution recovery fails
Problem: lease-blocked native navigation could leave a blank transcript, and cache failures could silently start full compatibility replay.
Root cause: passive readiness refresh switched startup-error tabs to controller-dependent Follow; activation failure settled independent history state. Native preparation did not distinguish ordinary errors from explicit unsupported formats, and root invalidation retained native watcher registrations.
Fix: coalesce matching cold reads, fence rebuild/reconnect paths, and let only read failures settle history errors. Restrict compatibility fallback to explicit unsupported results, positively identify ancient event envelopes, and remove replaced native watches.
Verification: deterministic activation regressions (59 assertions), frontend typecheck/lint, transcript suite, loading browser tests, focused agent/Desktop race tests and Go lint pass. Root full suite previously passed; Desktop default package timeout remains under investigation with exhaustive deterministic shards. Production history.15 failure is documented; new-package acceptance remains pending.
* refactor(history): share runtime rebuild subscription handling
Problem: the cold-read fence duplicated rebuild handling and exceeded the existing initial bundle budget by 380 bytes.
Root cause: single-tab and settings-wide rebuilds maintained equivalent subscription ownership logic in separate branches.
Fix: iterate the affected identities through one stop/invalidate/rebuild path, retaining the cold-read and activation guards without changing the asset budget.
Verification: activation regressions (59 assertions), prompt lifecycle tests and TypeScript checks pass; the complete production build will verify the unchanged bundle gate.
* fix(history): coalesce startup watches before resuming discovery
* fix(history): keep startup locale sync out of discovery
* feat(history): add shared on-demand native full-text search
* feat(history): add bounded discovery source diagnostics
* desktop: ignore noisy root watcher writes
* desktop: drain native watch backlog before initial history discovery
* refactor(history): remove retired startup migration workers
* fix(history): coalesce watcher overflow before journal writes
* fix(history): replace invalid scans at slice boundaries
Problem: large metadata discovery finished obsolete generations before replaying queued root changes, while unchanged rows rewrote topic projections.
Root cause: invalidations were consumed only at EOF and discovery always published full upserts.
Fix: supersede invalid iterators between slices with bounded coalescing; preserve incomplete journals and recheck unchanged rows transactionally before updating scan presence only.
Verification: sessioncatalog/historywork race suites, deterministic restart and projection regressions, Desktop watcher/startup race tests, and sessioncatalog lint pass. Native scale acceptance remains pending.
* perf(history): project registry metadata in bounded slices
Problem: registry refresh held the catalog writer for the entire registry and rewrote unchanged metadata.
Root cause: metadata-only discovery reused the atomic full-projection refresh and a whole-task timeout restarted large observations.
Fix: serialize cancellable observations, share background slice admission, compare persisted rows, defer paged retirement until input completes, and bound each writer transaction. Preserve atomic refresh for other catalog modes.
Verification: root Go suite, catalog/historywork race suites, Desktop catalog race coverage, frontend typecheck/transcript and loading/sidebar/navigation/scroll browser gates pass. Retain the preceding history.24 package's 90-run native scale evidence separately.
* feat(history): resume interrupted checkpoint index preparation
* test(history): verify checkpoint preparation across process exit
* feat(history): resume streamed legacy display index imports
* refactor(history): separate maintenance admission and publication steps
* feat(history): resume native event preparation across process exit
* fix(history): preserve cold reading windows during passive readiness refresh
* test(history): record production scale results and exercise native cache paging
* perf(history): load cold refresh proof on demand
* perf(history): defer session restoration policy with its reader proof
* perf(history): keep restoration policy outside the eager module graph
* feat(history): resume DAG preparation across all durable phases
* fix(history): include protected windows in resident cache capacity
* docs(history): document DAG resume and native cache findings
* fix(history): release evicted read bindings without affecting replacements
* test(history): qualify native cache resources and startup scale
* docs(history): record history.29 cache qualification
* test: align history migration fixtures with current schemas
Keep the junction migration regression focused on the last pre-invalidation schema and derive the projection schema assertion from the migration ledger. Complete the history hydration Meta fixture so frontend test type checking covers the current contract.
* fix: align startup bundle gate with measured producer output
Problem: the history startup PR exceeded the raw frontend bundle gate by 275 bytes on the Linux producer and the migration regression comment exceeded repolint's essay budget.
Root cause: the earlier budget ceiling was based on a pre-rebase producer measurement, while the test fixture note was four lines at a three-line position limit.
Fix: set the raw initial gate to the smallest measured 2056.6 KiB ceiling, keep all compressed and chunk budgets unchanged, and condense the migration note without removing coverage.
Verification: node scripts/check-bundle-budget.mjs; go run ./tools/repolint; focused sessioncatalog migration tests; git diff --check.
* fix: format desktop catalog runtime
Problem: the desktop producer gate rejected one unformatted composite literal in session catalog runtime code.
Root cause: the latest history loading changes left the pinned-only field alignment outside gofmt output.
Fix: apply the repository formatter without changing behavior.
Verification: gofmt -l ., git diff --check, and go run ./tools/repolint.
* fix: use passing producer bundle ceiling
Problem: the Desktop producer still exceeded the raw startup bundle gate by 112 bytes after the previous measured adjustment.
Root cause: the Linux CI build measured 2106070 bytes while the 2056.6 KiB ceiling allowed only 2105958 bytes.
Fix: set the raw initial budget to the smallest 2056.8 KiB ceiling that contains the producer result; compressed, chunk, CSS, locale, and residency budgets remain unchanged.
Verification: node scripts/check-bundle-budget.mjs, go run ./tools/repolint, gofmt -l ., and git diff --check.
* fix: allow Windows history sources to be replaced during paging
Problem: Windows pager fencing tests failed when a source or event log was replaced while a display index was being built.\n\nRoot cause: os.Open uses a Windows sharing mode that denies delete and rename, so the deterministic source-change interleaving failed before the pager could validate the new pathname identity.\n\nFix: add a platform-owned replaceable read helper. Windows opens sources with read, write, and delete sharing; Unix keeps the ordinary descriptor. All legacy pager builders, event-kind probing, and paged event/DAG reads use the shared helper.\n\nVerification: go test ./internal/agent -count=1; GOOS=windows GOARCH=amd64 go test -c ./internal/agent; go run ./tools/repolint; go test ./...; git diff --check.
* fix: match Windows replaceable read handle flags
Problem: the first shared-delete handle still left the DAG source replacement interleaving failing on Windows.\n\nRoot cause: the native source handle did not use the same reparse-aware backup semantics as the repository's proven replaceable regular-file reader.\n\nFix: align the fileops helper flags with the existing Windows repair reader while retaining read, write, and delete sharing.\n\nVerification: GOOS=windows GOARCH=amd64 go test -c ./internal/agent; focused pager tests pass on the host.
* fix: grant Windows pager handles replace access
Problem: atomic replacement of a DAG event log was still denied on Windows despite delete sharing.\n\nRoot cause: the replacement operation also needs delete access on the existing native source handle.\n\nFix: request DELETE together with GENERIC_READ while retaining read, write, and delete sharing.\n\nVerification: GOOS=windows GOARCH=amd64 go test -c ./internal/agent.
* fix: make Windows source replacement regression portable
Problem: the pager source-fencing regression used os.Rename(temp, existing) while the target was open, which Windows rejects; granting DELETE access then broke ordinary concurrent rewrites.\n\nRoot cause: the test encoded Unix replacement semantics that are unavailable for an open Windows target, while the production reader must remain compatible with normal writers.\n\nFix: keep the shared-delete read handle at read access and model replacement by moving the old pathname aside before installing the new file. The test still verifies a changed native identity is rejected before publication.\n\nVerification: focused pager source-change tests, GOOS=windows GOARCH=amd64 go test -c ./internal/agent, go run ./tools/repolint, and git diff --check.
* fix: join startup cold history on runtime ready
Problem
A runtime-ready event could start ordinary session hydration while startup cold history was still in flight, issuing a duplicate history read for the same session.
Root cause
The ready reconciliation only shared the normal session-load owner. Cold startup reads were tracked separately, so the readiness path did not join that owner.
Fix
Join matching cold-history promises before starting ordinary hydration. After the certified window settles, install the live follower and continue ancillary hydration with the cached window. Keep the failure path on the existing reader and record the measured 163-byte startup bundle increase with the smallest producer-safe budget update.
Verification
- pnpm exec tsx src/__tests__/ready-event-meta-sync.test.tsx
- pnpm typecheck
- pnpm test:typecheck
- pnpm lint:hooks
- pnpm test:transcript
- pnpm build
- go run ./tools/repolint
- git diff --check
* Fix protected history windows and catalog context races
Problem
Navigation could install a transcript before the active tab was pinned, allowing the resident cache budget to evict the just-opened window. Catalog callbacks could also reread the mutable App context while the lifecycle test replaced it, producing a data race.
Root cause
Active-tab pinning was deferred to a post-paint effect, and the restored catalog watcher used the App context accessor instead of its owning worker context.
Fix
Publish the active pin at the dispatch boundary before the first history page commits. Let the catalog watcher pass its existing context into historical discovery while retaining the default accessor for synchronous callers.
Verification
- tab-switch-hydration: 101 passed
- desktop TestConcurrentRebuildSessionCatalogCallersShareFailure with -race: 3 runs passed
- desktop TestHistorySliceSourceField/cold_index_hit: 100 runs passed
- pnpm typecheck and pnpm test:typecheck passed
- pnpm lint:hooks and pnpm test:transcript passed
- go run ./tools/repolint passed
- git diff --check passed
* Fix Windows history upgrade acceptance and test cleanup
Problem: Windows CI rejected visible upgraded history and could not remove a test source directory.
Root cause: the UI acceptance helper required an activation button even after lazy history had rendered, while the test closed its historical service after TempDir cleanup.
Fix: accept a healthy visible transcript without activation and close the test app services before removing the source directory. Keep pending and recovery-state rejection covered.
Verification: focused desktop Go test, go vet, repolint, golangci-lint, and diff checks passed. Windows PowerShell and installer checks remain for CI.
* Align history CI checks with lazy legacy loading
Problem: the Desktop CI matrix failed startup worker, cold index, and packaged upgrade checks after history loading changed.
Root cause: those checks still assumed startup discovery took the old coordinator lock, equal-timestamp sidecars were trustworthy, and opening a legacy tab automatically migrated it without rewriting JSONL. The current shutdown path snapshots an active legacy tab.
Fix: deterministically block the actual startup worker, establish sidecar ordering for the index-hit fixture, and verify legacy path, authored content, registry non-import, restart, and backups in the packaged fixture.
Verification: focused desktop tests with repeated and race runs, upgradefixture tests, go vet, repolint, golangci-lint, and diff checks passed. Windows packaged acceptance remains the terminal CI gate.
Problem: guidance could fail before persistence when turn discovery was missing, or refresh to a successor turn. Lost replies could also cause duplicate submissions, while session exports lacked transport evidence for silent requests.
Root cause: callers mixed observed turn identity with current tab discovery and did not share durable receipt ownership; diagnostics observed model attempts without request/header/body stages.
Fix: preserve captured session targets and observed turns, queue unavailable-turn guidance durably, and retain original idempotency keys for uncertain receipt recovery across entry points. Add bounded, content-free provider transport snapshots. Keep action-only request construction in the lazy submission owner. Remove an unrelated disk-speed cutoff from the long-history reachability test while retaining its correctness assertions and package timeout.
Verification: root, desktop, frontend, provider/control race, production build, and repository lint coverage passed. Final PR CI completed successfully across Linux, Windows, macOS, browser/native desktop, and independent memory screening. Provider-visible serialization, cache inputs, retry policy, and timeouts are unchanged.
* fix: resolve Windows junction identities without breaking upgrade locks
Problem: Windows users whose existing data directory is a junction cannot
start sessions after configuration locks begin requiring physical identity.
Recovery also labels runtime-start failures as failed draft saves.
Root cause: EvalSymlinks can leave a junction leaf unchanged and reject its
children, splitting aliases or failing before the OS lock is acquired.
Fix: resolve existing Windows paths through native metadata handles, retain
legacy workspace lock spellings, isolate/rebuild derived catalog identities,
and separate historical draft submission errors from persistence errors.
Add a read-only probe and native regressions to the Windows PR selectors.
Preserve current eager session creation and authoritative user data formats.
Verification: owning Go suites, host/Windows golangci-lint, repolint, frontend
production/test typechecks, draft ownership and recovery regressions, real
Chromium recovery interactions, and Windows selector coverage passed.
Native Windows execution and complete upgrade qualification remain CI/manual
gates; cross-compilation does not establish those results.
* fix: preserve native Windows error kinds and verify historical lock spelling
Problem: native Windows CI rejected dangling-link and loop error kinds, and
the legacy-lock fixture expected the unexpanded runner temporary path.
The recovery UI also exceeded an almost exact startup-byte budget.
Root cause: os.IsNotExist does not traverse arbitrary wrapping, native
reparse exhaustion uses ERROR_CANT_RESOLVE_FILENAME, and EvalSymlinks expands
8.3 ancestors even when it preserves a junction leaf.
Fix: use wrapped-error matching and the native reparse-loop code; derive the
fixture's legacy key with the historical filesystem algorithm. Keep all
rejection and lock assertions. Remove a duplicated browser invocation.
Adjust only the raw startup ceiling to the measured producer size: same
source/dependencies/commit stamping adds 506 B raw (0.024%) and 120 B gzip;
Linux Electron measures 2103802 B, rounded to 2054.5 KiB.
Verification: pathidentity, workspacelease and probe tests; host/Windows
lint; repolint; bundle budget; same-toolchain base/candidate production builds.
All compressed, chunk, CSS, locale and residency budgets remain unchanged.
* test: exercise native junction identity across Windows volumes
Problem: the reported regression crosses system and data volumes, while the
initial regression fixtures only created same-volume junctions.
Root cause: filesystem alias tests did not exercise drive-prefix changes.
Fix: create a temporary junction from the system temp drive to RUNNER_TEMP
and compare entry and missing-tail identities with the physical target.
Require the two-volume fixture on GitHub Windows runners. Single-volume
local machines still run the unconditional same-volume regression suite.
Verification: Windows-target lint and cross-compilation; the native Windows
PR lane is required to execute the cross-volume fixture.
* Preserve credential ACL recovery and bind artifact consumers to producer Node
Problem: native identity resolution blocked credential ACL repair before edit locking, and prepared frontend consumers selected different Node patch versions.
Root cause: FILE_READ_ATTRIBUTES requires permissions that legacy read-deny ACLs remove; floating Node majors resolve independently on hosted runners.
Fix: use zero-access metadata handles, keep read denials intact until credential recovery, cover linked and unrecorded ACL cases, and propagate the producer Node version to every artifact consumer without weakening verification.
Verification: pathidentity/config tests, Windows cross-compilation and golangci-lint, 40 workflow/artifact/selector tests, repolint and diff checks passed. Native Windows execution remains a CI gate.
* Bind memory screening consumers to their build Node version
Problem: a memory shard rejected the prepared build before running because its floating Node major selected a different patch version.
Root cause: the independent app-memory workflow owns a second producer-consumer identity contract.
Fix: propagate its producer runtime to shards and aggregation, preserving exact identity validation. Add a workflow regression for both consumers.
Verification: 51 workflow and memory protocol tests, repolint, and diff checks passed.
* Open Windows identity handles with exact native access rights
Problem: CreateFile with zero desired access still fails against legacy credential DENY RX ACLs.
Root cause: Win32 adds SYNCHRONIZE and FILE_READ_ATTRIBUTES, as already documented by the credential ACL recovery owner.
Fix: open a zero-access NtCreateFile handle that follows reparse points, use the native namespace for long/UNC paths, and translate NTSTATUS failures back to Win32 codes for existing error classification.
Verification: Windows compilation and golangci-lint, repolint, and diff checks passed. Existing save/load/revision tests require identity to resolve while content reads remain denied; native CI verifies this contract.
* Request valid metadata access for native Windows identity queries
Problem: NtCreateFile rejects a zero access mask, breaking ordinary Windows identity queries.
Fix: request FILE_READ_ATTRIBUTES without implicit synchronization; when an ACL denies that right, try READ_CONTROL, which file owners retain for security metadata. Neither handle grants content access or mutates the ACL. Preserve NTSTATUS classification and all denial assertions.
Verification: Windows compilation and golangci-lint, repolint and diff checks passed; Windows execution is required before qualification.
Problem: region-first filtering hid MiMo API versus Token Plan access and new connections retained the V2.5 model catalog.
Root cause: product selection depended on region, MiMo lacked a one-time model migration, and floating CI Node versions made frontend artifact producers and consumers disagree.
Fix: select access type before cluster and protocol, clarify independent credentials and usage, default new connections to V2.6, and atomically append models to eligible official connections without changing existing defaults or custom data. Preserve previous-writer TOML compatibility, share repeated frontend defaults within existing budgets, and pin artifact workflows to one exact Node runtime.
Verification: root and Desktop suites, previous-reader migration round trips, frontend and browser regressions, unchanged production bundle budgets, cross-platform CI, race checks, Windows packaging, memory screening, CodeQL, and release-control contracts passed. All review threads were clear before merge.
* fix(desktop): restore eager session creation with durable input recovery
Problem: deferred draft creation changed explicit New Session behavior and left upgraded users dependent on draft-only input and recovery paths.
Root cause: manual creation, composer persistence, and legacy draft submission shared identities and lifecycle assumptions that no longer match formal sessions.
Fix: create formal sessions through durable idempotent operations, persist unsent input separately with revision and receipt fencing, retain legacy drafts as recovery-only data, and retire automatic empty-session cleanup. Repair model, attachment, Goal, and submission wiring while preserving current canonical storage and history formats.
Verification: tagged 1.38.3 through 1.38.11 compatibility fixtures; all 11 desktop test partitions; focused race, composer and lifecycle regressions; source and test type checks; repository lint; signed macOS arm64 packaging and isolated restart/image/lifecycle smoke tests. Native Windows and Linux application validation remains pending.
* fix: repair rollback CI gates and confine session deletion
Problem: rollback CI failed lint and the frontend producer gate, while CodeQL reported existing deletion paths outside a rooted filesystem boundary.
Root cause: reserved initialization ignored binding release errors, historical tests used outdated lint syntax, and the raw budget sat below the measured main-v2 baseline. Deletion rebuilt absolute trash and cache paths after validating the session identity.
Fix: join release errors, modernize fixture loops without removing assertions, use rooted rename and removal for session deletion, and round only the initial raw bundle gate from 2053.9 to 2054.1 KiB. Same-toolchain main-v2 measures 2103208 bytes versus 2103296 bytes for this branch, an 88-byte increase.
Verification: root and Desktop golangci-lint 2.12.2, Desktop Windows lint, reserved initialization and historical fixture regressions, deletion confinement tests, production bundle guards, and repolint passed. No compatibility coverage was removed.
* test: preserve historical fixture bytes across Windows checkout
Problem: all Windows history partitions failed provenance hashes, and frontend CI still asserted the retired draft-on-new behavior.
Root cause: Git autocrlf rewrote JSONL and metadata bytes emitted by tagged writers. A source-regex chrome assertion duplicated navigation coverage while encoding the superseded product contract.
Fix: disable text conversion only for the two historical fixture trees, retaining original hashes and offsets. Remove the obsolete regex assertion; formal-creation, legacy recovery, remote navigation, and intent-fencing behavior tests remain.
Verification: a core.autocrlf=true checkout preserved all 264 tagged historical file hashes, and app-chrome tests passed. The complete frontend CI runner is running locally; replacement Windows CI will verify the native migration matrix.
* test: align recovery assertions and native observation lifecycle
Problem: frontend tests still expected active empty-session cleanup and hidden content-free drafts; the native browser smoke timed out reading its initial hidden macOS page.
Root cause: those assertions retained the retired draft-first product contract. The native fixture bypassed the observation lease that production host calls acquire before executing DOM scripts.
Fix: assert that cleanup cannot be resumed while manual restore remains available, keep settings-only legacy drafts discoverable with workspace isolation, and acquire/release the existing observation lease for the initial hidden-page snapshot and fixture setup.
Verification: all 505 frontend CI commands passed across the unchanged completed set and remaining set; Electron typecheck and repolint passed. Native macOS browser smoke passed 100 lifecycle cycles with no remaining pages. Timeouts, product code, and coverage counts are unchanged.
* fix: preserve canonical inbox identity and settled tool status
Problem: canonical sessions could reject queued messages or count another
session's background jobs, while settled tool rows could remain running.
Diagnostic exports could also stop when text redaction damaged JSON.
Root cause: logical session locators were conflated with backing paths, and
sampled job states were treated as the output reader's invocation state.
Diagnostic redaction operated on serialized JSON instead of string values.
Fix: separate inbox identity from storage, use canonical identities for
receipt ownership and runtime metadata, settle background launch receipts,
retain output-reader completion during history restore, and redact JSON
structurally without changing counters or the persisted inbox format.
Verification: root Go suite; focused control and desktop race tests;
sessioninbox and secrets tests; frontend typecheck and transcript suite;
Chromium transcript regression; repolint and git diff --check passed.
* fix: close query workers and reconcile CI validation
Problem: Linux CI raced temporary-directory cleanup with query-cache writes,
and desktop preparation exceeded its initial raw bundle gate by 35 bytes.
Root cause: the mirror-listing fixture retired runtimes without shutting down
its service-owned query workers. Settled tool-state handling adds 104 bytes
to the same-toolchain base and consumes the previous rounded payload margin.
Fix: register terminal service shutdown before directory cleanup and isolate
the listing regression in its own test file. Round only the measured initial
raw bundle budget from 2053.8 to 2053.9 KiB; keep all other gates unchanged.
Also document the unchanged user workflow through PR impact metadata.
Verification: the listing regression passed 20 race-enabled repetitions;
identity lifecycle race tests, transcript and submission regressions, manual
compaction regressions, repolint, and documentation impact validation passed.
Both base and candidate Electron bundles were measured with one toolchain.
* fix: order snapshot eviction independently of clock precision
Problem: Windows CI retained an old read handle after the snapshot bound
was reached, failing the read-snapshot eviction regression.
Root cause: multiple accesses can share one clock sample. Comparing only
timestamps made LRU selection depend on nondeterministic map traversal.
Fix: assign a monotonic access sequence under the store lock at publication,
handle acquisition, and page access. Use that order for eviction and retain
elapsed time for idle and absolute expiry.
Verification: a forced-equal-timestamp regression covering insertion and
page promotion fails against the old implementation. The read-snapshot
suite passed five race-enabled repetitions; desktop go vet and repolint
passed. Native Windows validation remains in the replacement CI run.
* test: range directly over snapshot eviction expectations
Problem: CI's Windows desktop lint rejected the eviction regression loop.
Root cause: the test ranged over len(ordered) instead of the slice itself,
which violates the intrange rule enforced by the pinned linter.
Fix: range directly over ordered, preserving the same eviction assertions.
Verification: run golangci-lint 2.12.2 for Windows amd64, matching CI.
Problem: Graphics failures can leave the Desktop interface unusable before users can reach hardware acceleration settings.
Root cause: Renderer reloads had no bounded recovery policy, and explicit GPU failure evidence was not retained across launches.
Fix: Classify GPU and renderer failures, bound reloads, provide native compatibility restart with draft-loss confirmation, and retain versioned local fault records. Keep acceleration enabled by default and let users choose temporary or persistent software rendering. Preserve newer faults during dialog acknowledgement and rearm prompts after healthy recovery.
Verification: Electron unit tests, deterministic recovery and lifecycle regressions, typechecks, shell build, repository lint, CI workflow tests, and real macOS Electron recovery smoke tests passed. Windows recovery smoke coverage is wired into CI; real GPU driver failure reproduction remains unavailable.
* Fix manual compaction feedback and history detection
Problem:
The /compact command gave no immediate acknowledgement while admission was pending, and sessions could report no compactable history even when completed turns existed.
Root cause:
The composer derived pending compaction state from mutable draft text, while manual compaction bypassed the shared recovery path when automatic compaction had no configured context window. Forced tail sizing also counted fixed context as conversation history.
Fix:
Capture submission identity with the source draft, preserve edits made during admission, show pending compaction progress, and route explicit compaction through the normal overflow recovery ladder. Size forced retention from summarizable history only.
Verification:
Ran root Go tests, focused agent and control tests, frontend type checks, maintenance browser coverage, transcript tests, CSS validation, WAAPI validation, and scroll-writer validation.
* Satisfy the Composer file-size budget
Problem:
The repository lint check rejected the compaction fix because Composer.tsx exceeded its baselined file-size budget.
Root cause:
The initial implementation added seven more physical lines than the legacy oversized-file allowance permits.
Fix:
Keep the submission-kind state inline and compact the new admission expression so the file stays within its existing budget without changing behavior.
Verification:
Ran go run ./tools/repolint, frontend type checks, and the maintenance browser test.
* Keep compaction feedback within the bundle budget
Problem:
The app-memory workflow stopped during its frontend preparation because the new compaction feedback exceeded the initial raw bundle budget.
Root cause:
The first version added verbose localized hint text and redundant pending-state styling to a bundle already close to its enforced ceiling.
Fix:
Shorten the hint while preserving its guidance, reuse the existing compaction icon for progress, and remove CSS declarations already supplied by the parent layout.
Verification:
Ran the full frontend production build and bundle budget, maintenance browser and card tests, and repository lint.
* Record the compaction feedback bundle budget
Problem:
The Electron release build rejected the intended compaction progress feedback even after redundant UI bytes were removed.
Root cause:
The raw startup budget tracks measured feature growth in 0.1 KiB increments and had no remaining headroom for the new admission status and running hint.
Fix:
Record the Linux Electron producer measurement and raise only the raw startup ceiling from 2053.5 KiB to 2053.8 KiB. Keep every compressed, chunk, CSS, locale, and resident-history gate unchanged.
Verification:
Ran the Electron release build with a full REASONIX_COMMIT, repository lint, maintenance browser and card tests, and confirmed the merged GitHub Ubuntu test job passed.
Problem: The Electron shell replaced the packaged macOS Dock icon with the full-canvas cross-platform PNG, making Reasonix appear larger than neighboring apps.
Root cause: Dock icon selection shared the Windows/Linux window-icon candidates and ignored whether Electron was running from a packaged app bundle.
Fix: Preserve the packaged ICNS, use matching inset artwork only during development, and cover both selection modes plus asset parity.
Verification: pnpm test; pnpm typecheck; go test appicon_asset_test.go -run 'TestDarwin' -count=1; native Electron Dock icon loading check.
* fix(transcript): keep a user row ahead of its live output
Problem:
Streamed reasoning, answer text, and tool calls could render above the
user message that started the turn. Later segments of the same turn
then continued below the bubble.
Root cause:
acceptBusiness copied a business record into the follower change before
filling an empty TurnID. Export and reload read the buffer, which had
the turn identity; the live change did not. Desktop attaches
already-mounted output to that turn only when the durable user row
carries the same turnId, so the output stayed on the previous formal
anchor or ahead of the prompt.
Fix:
Publish the assigned TurnID with the change record. Place live rows
after the submission anchor captured at send time when that user row
has no turnId, and move same-turn formal output back behind its user.
Keep a resident turnId when a projection update omits it, and stamp the
active turn onto a live answer that arrived before the identity did.
Verification:
go test ./internal/transcript/ -count=1 -run TestAcceptBusinessPublishesTheTurnIDItAssigns
desktop/frontend src/__tests__/submission-handoff.test.ts
* Preserve sampling order and the offscreen reader during transcript settlement
Problem: assigning turn identities could move later live answers ahead of
earlier samples and tools. Batched deltas could also clear the newer-history
flag while the reader remained on an older page.
Root cause: the user anchor overrode every existing formal predecessor, and
predecessor lookup ignored newly settled rows while retaining reclaimed ones.
The offscreen batch path leaked a temporary history flag into visible state.
Fix: preserve surviving and newly formal predecessors within the owning user
segment, and keep the reader's history flag while updating its offscreen tail.
Document both invariants and add six deterministic regressions. Five fail on
the previous PR head; all pass after the fix, including the ordinary-event
sibling of the batched stream path.
The production raw bundle measures 2102574 B versus 2100795 B on the base
(+1779 B, 0.085%). Set only its existing ratchet to 2053.5 KiB, preserving the
established 186 B Linux producer difference. Other budgets are unchanged.
Verification: transcript Go package; frontend test:typecheck; submission
handoff and output-order suites; test:transcript; submission and transcript
Chromium suites; production build and check:bundle; repolint; diff check.
* feat(browser): add task-bound runtime and scoped diagnostic evidence
Problem: production page injection and invalid captures prevent reliable browser inspection, while session exports lack browser troubleshooting evidence.
Root cause: host-coupled page scripts, missing render ownership and mixed viewport/input facts allow stale observations and callbacks across lifecycle changes.
Fix: bundle isolated semantic scripts; coordinate rendering, cancellation and identity; validate images; add optional query, wait, responsive, picker, diagnostics, recording and recovery capabilities; export bounded session-owned evidence without changing standing tool schemas.
Verification: 279 Electron tests and typecheck; prior full Go and native/package evidence documented in the acceptance report. Native OOPIF CDP stalls and expanded scaled-input routing remain explicit merge blockers; no retry or deadline workaround hides them.
* fix(browser): preserve cancelled capability write error chains
Problem: capability transport failures lost their underlying cancellation identity, and generated Desktop inventory locations were stale.
Root cause: the unknown-outcome wrapper formatted the original error as text instead of wrapping it; browser diagnostic additions shifted inventory source positions.
Fix: wrap both the unknown-outcome sentinel and original transport error, and regenerate the existing Desktop inventory.
Verification: a server-admission cancellation regression failed before the fix and now passes for pointer, viewport and recording writes; browser tests, root/Desktop golangci-lint and inventory tests pass.
* fix(browser): route pointer input through the owning renderer
Problem: zoom and native surface changes could drop clicks or send their press and release to different frames.
Root cause: root CDP input used asynchronous cross-process hit-test data from the previous surface; DOM geometry alone did not establish input routing readiness.
Fix: resolve the current frame in isolated worlds, use direct native input for in-process targets and the owning CDP session for OOPIFs, preserve coordinate scaling, and fence preparation against cancellation and identity changes. Strengthen diagnostic export URL assertions and align the native diagnostic fixture with host observation leases.
Verification: 284 Electron tests, TypeScript and production build pass; macOS production fixture passes three scale transitions and 100 mixed-frame lifecycle cycles. Windows ARM64 and Linux ARM64/Xvfb pass the renderer-owned input matrix. Historical CDP stalls remain a separate merge gate.
* fix(browser): own iframe sessions for the debugger connection lifetime
Problem: adjacent frame observations and actions repeatedly tore down and initialized the same OOPIF protocol domains, exposing world-creation and initialization stalls.
Root cause: target attachment ownership was tied to individual operations instead of the page debugger generation; cancellation and shared initialization therefore lacked a common owner.
Fix: pool target sessions per connection, retain operation-scoped contexts and object groups, retire failed sessions after their last borrower, and clean late attachments without affecting replacement sessions. Preserve the existing one-second protocol budget and unknown write outcomes.
Verification: 287 Electron tests and TypeScript pass; deterministic shared-waiter, cancellation, late-attach and cleanup-order regressions pass. Native macOS validates one attachment across snapshot/ref/input/upload, a fresh attachment after idle reconnect, the scale matrix and 100 mixed-frame lifecycle cycles. Windows ARM64 and Linux ARM64/Xvfb native fixtures pass.
* fix(browser): preserve renderer-local pointer coordinates
Problem: Scaled iframe clicks could hit a broad button away from its observed centre, and nested same-process descendants used the wrong coordinate origin.
Root cause: Child CDP sessions accept renderer-local CSS coordinates, but input reapplied outer Fit scaling and treated the innermost frame as the renderer owner.
Fix: Carry the session-owning frame through the operation runtime and dispatch its unscaled traversal point. Native fixtures assert exact received coordinates for same-process descendants inside OOPIFs.
Verification: 288 Electron tests, typecheck, macOS 100-cycle native fixture, Windows ARM64 and Linux ARM64 native matrices, and the complete macOS packaged browser toolchain passed.
* fix(browser): publish recording completion after cleanup
Problem: macOS native CI could reject a new recording immediately after the previous job reported completed.
Root cause: WebM validation published terminal status before asynchronous file cleanup released the global recording slot.
Fix: Keep cleanup in finalizing and publish the terminal result and artifact at the same synchronous boundary that releases admission. Apply the same invariant to cancellation and interruption.
Verification: A deterministic cleanup barrier fails on the prior implementation and passes for completed and cancelled jobs. All 289 Electron tests and TypeScript checks pass; native and packaged validation continue on this head.
* test(cli): join controller teardown before removing fixtures
Problem: Windows CI failed removing a clear-command fixture because its recovery database was still open.
Root cause: The CLI controller owner helper only requested asynchronous Close; a Running poll did not join final persistence and store release.
Fix: Wait for the existing Closed signal in the shared test owner, matching control tests, and remove the redundant timed cancellation poll.
Verification: Complete internal/cli tests, focused race tests, and Windows ARM64 clear/cancel native test cases passed. Product behavior and test deadlines are unchanged.
* fix(desktop): decouple history navigation from runtime startup
Problem:
Opening a saved canonical conversation can remain unresponsive even when
its persisted history is independently readable.
Root cause:
Canonical topic activation synchronously resumed the execution controller
before returning the identity needed by frontend history readers. Deferred
surface pruning could also hold the navigation lock while waiting for
runtime mutation. Rapid return clicks could reuse an already-cancelled
startup that would never publish a controller.
Fix:
Validate canonical membership and publish its identity through the shared
asynchronous topic-open lifecycle. Preserve history access on startup or
writer-ownership failure. Do not reuse cancelled startup shells. Acquire
async prune locks without waiting for runtime work under the navigation
lock, then revalidate the activation generation before removing surfaces.
Keep synchronous OpenSession and legacy prune callers on their contracts.
Verification:
Deterministic regressions reproduce blocked startup, blocked prior prune,
and cancelled-startup reselection. Focused desktop race tests, frontend
topic activation/navigation tests, transcript tests and Chromium transcript
checks pass. Root make lint, repolint and desktop golangci-lint pass.
Windows installed-package reproduction remains unverified.
* fix(desktop): read committed runtime states for history lists
History lists collect status from all open and detached controllers. A fresh
sample waiting on an unrelated controller could therefore block another
project's history query before its catalog deadline was established.
Publish an owned immutable snapshot at the controller's semantic commit
boundary and prefer that observation interface in Desktop. Reuse an existing
snapshot when building project nodes instead of sampling twice. Preserve the
fresh-sampling interface for existing callers and embedders.
Cover blocked producers, existing history identity, indexed and fallback
lists, open and detached sessions, cancellation, completion, and ownership of
mutable observations. The same list regression fails with the old reader.
Validation: root go test ./...; full control race suite; 129 Desktop targeted
tests/subtests under race; final four-case history regression and published
state tests; three frontend lifecycle checks; both module linters; repolint.
* fix(desktop): make history lifecycle cancellation and shutdown retryable
Problem:
History project lists could be blocked by live runtime sampling, Stop could wait behind the same observation path, and shutdown could report success while a session runtime remained active.
Root cause:
History loading mixed authoritative session identity with mutable runtime observation, while cancellation and service shutdown treated observation or cleanup errors as part of a best-effort path. The frontend also lacked request-stage evidence for distinguishing queueing, backend latency, and stale-result disposal.
Fix:
Use published runtime snapshots for history and Stop metadata, signal cancellation before observation, preserve failed shutdown checkpoints for retry, and record bounded privacy-safe history-list request stages. Add deterministic cancellation and shutdown regressions plus bilingual runtime-state documentation.
Verification:
- go test ./...
- go test -race . -run 'Test(Shutdown|ConnectionLoss|DetachedShutdown|ReleasedBinding|CancelSession)' -count=1
- go test -race ./internal/control -run 'Test(Cancel|Maintenance|StaleCancelling|PublishedRuntime)' -count=1
- pnpm typecheck, pnpm lint:hooks, pnpm build
- go run ./tools/repolint
* fix(desktop): reconcile history diagnostics with snapshot pagination
Problem: Latest main-v2 snapshot pagination paths had lost request completion diagnostics during merge reconciliation, and the legacy shutdown wrapper did not expose cleanup failures.\n\nRoot cause: The conflict resolution selected the newer snapshot reader without restoring the lifecycle diagnostics and result-returning shutdown helper.\n\nFix: Restore stale/completed project-tree request events for every accepted or discarded page, including preserved complete pages, and route the compatibility shutdown wrapper through the error-aware result path.\n\nVerification: go test -race ./desktop -run 'Test(Shutdown|ConnectionLoss|DetachedShutdown|ReleasedBinding|CancelSession)' -count=1; cd desktop/frontend && pnpm typecheck && pnpm lint:hooks && pnpm build; go run ./tools/repolint
* fix(ci): remove flaky recovery observer deadline
Problem: PR CI failed in golangci-lint on wrapped runtime-binding errors and Windows intermittently timed out while observing a valid recovery publication.\n\nRoot cause: The shutdown classifier used direct equality instead of errors.Is, while the watchdog regression used a five-second runtime observer deadline that also measured platform-dependent durable I/O latency.\n\nFix: Preserve wrapped released-binding classification with errors.Is and wait for the watchdog's durable terminal event before asserting recovery state, removing the unrelated observer timing budget while retaining all commit, recovery, late-worker, and admission assertions.\n\nVerification: go test -race ./internal/control -run 'Test(TerminationCommitWatchdogRejectsLateWorker|CancellationGraceSealsUncooperativeTurnAndPreservesQueue|RecoverySealDropsLateTranscriptOutput|CancelSessionAcknowledgesBeforeStatusBarrier)' -count=5 -timeout=2m; cd desktop && go test -race . -run 'Test(ShutdownServiceFailurePreservesEvidenceAndRetries|ReleasedBindingWarningDoesNotHideOtherShutdownErrors)' -count=1; golangci-lint run --timeout=5m ./...; go run ./tools/repolint
* ci(desktop): retry transient Electron binary fetches
Problem: The macOS desktop job failed while Electron's lazy binary download received an HTTP 504 from the GitHub release endpoint, after all product tests had passed.\n\nRoot cause: The CI step treated a transient dependency transport failure as a deterministic Electron diagnostics failure.\n\nFix: Retry only Electron binary installation errors caused by HTTP 429/5xx responses, up to three bounded attempts; all other performance-smoke failures still exit immediately and remain fatal.\n\nVerification: Reproduced the exact 504 failure from the macOS job log, validated the workflow diff with git diff --check, and preserved the three-minute step budget and product assertions.
Two halves of the host-lifetime watcher could not be unit tested: any real
service spawns a helper through os.Executable(), and in a package test binary
that child is the test binary itself — it would re-run the suite. A zero-value
Service cannot stand in either: Service.backend is an interface, so Close()
ends at s.backend.close() on nil.
Options.ScanOnly refuses every registration, which is how the service already
handles a backend it cannot use: the root degrades to backoff scanning. A test
can therefore hold a real, closable service with no process, and both halves
are now pinned:
- a build must not close a caller-owned service, and must close one it created
- the host closes its watcher at shutdown
Each was verified to fail when the corresponding guard is removed.
shutdownStatus carries no step names, but the coordinator records them in
finished, which a package test can read. This fails if the step is dropped or
renamed, which is the wiring half of the shutdown path. The other half — a real
helper dying — needs a real service, and a test binary cannot create one.
Two halves of the host-lifetime watcher are cheap to test without spawning a
helper: the App reuses an existing service instead of replacing it (a second
service is a second helper process), and a package test binary never creates one
(that helper would be this test binary, re-running the suite in a child).
Both were verified to fail when the implementation is broken.
BuildRuntime created a skill-watch service unconditionally, and
skillwatch.NewService spawns its helper process in the constructor. So every
controller build spawned a 56MB reasonix-desktop.exe helper, and because a
helper lives until its build is closed, rebuilds left several resident at once.
Measured on the desktop before this change: one helper spawn per
OpenSession/ArchiveSessionTarget (1:1), 24 spawns during a three-minute
archiving session, and two helpers resident for 16 minutes.
The service is now caller-owned, mirroring Options.SharedHost: pass
Options.SharedSkillWatchService and Build subscribes this controller's stores
to it instead of creating one. The desktop creates one per App and closes it at
shutdown. A build-owned service is still created and closed as before, so every
other frontend is unchanged.
boot.NewHostSkillWatchService applies the same production-binary gate, so a
package test binary still spawns no helpers.
Regenerate the combined Desktop host contract and migration inventory to preserve both snapshot pagination and queued guidance operations. Verified frontend build and bundle limits, snapshot and inbox lifecycle tests, Desktop contract tests, repolint, and inventory freshness.
* feat(desktop): edit and reorder queued guidance
Problem: queued guidance could not be safely edited or reordered, and edit errors could strand the user draft.
Root cause: the inbox had no versioned edit/move contract or atomic dispatch check; the composer also mixed current-turn guidance with follow-up queue submission.
Fix: add versioned store/controller and local/remote queue commands, an inline editor with draft recovery and sorting, explicit current-turn guidance, pause controls, and lifecycle fences for delayed replies.
Verification: store/controller/Desktop focused tests, race interleavings, frontend typecheck and queue integration tests, production build, and browser preview interaction checks passed locally.
* Fix queue preview test setup and desktop inventory
Problem: Initial PR checks failed in frontend Node tests and desktop inventory validation.
Root cause: The browser preview assumed Vite env in Node, lazy asset hooks registered after tsx, legacy composer fixtures lacked authoritative queue state, and the generated desktop command inventory was stale.
Fix: Guard preview env, preserve optional mock queue capabilities, preload CSS/SVG hooks before tsx, align composer fixtures with next-turn queue semantics, and regenerate inventory.
Verification: 433/434 discovered suites passed before the final remote presentation assertion was aligned; the remaining suite passed in isolation. Typecheck, build, focused Go tests, and inventory validation passed.
* Keep composer fixtures within repository size budgets
Compact the new queue mock state in the existing composer tests without removing assertions. The published head exceeded repolint's existing per-file budget; go run ./tools/repolint and both affected test suites now pass.
* Avoid synthetic Inbox bindings in browser mocks
Inbox capabilities are supplied by the selected host or preview fixture. Skip the attachment fallback for absent Inbox methods, which also keeps the initial raw bundle within the existing budget. Verified with pnpm build and the composer queue fixture.
Resolve the frontend bundle ratchet from the measured combined build and regenerate the Desktop host contract from the merged Go API. Preserve both snapshot pagination and the latest compaction receipt/provider catalog changes.
Problem: the Windows workspace lease test released its first owner after an arbitrary 100 ms delay, and the archive wiring test required an obsolete untrimmed session-path expression.
Root cause: runner scheduling could let the second lease attempt start only after release, while the source assertion drifted behind the stronger normalized path implementation.
Fix: synchronize release on the actual wait callback with cleanup fallbacks, and assert the trimmed session-path wiring that the archive menu now uses.
Verification: go test ./internal/workspacelease -run ^TestOwnersSerializeSameWorkspaceAndNotifyOnce$ -count=100; go test -race ./internal/workspacelease -run ^TestOwnersSerializeSameWorkspaceAndNotifyOnce$ -count=20; golangci-lint run --timeout=5m ./internal/workspacelease/...; pnpm exec tsx src/__tests__/project-tree-archive-race.test.ts; pnpm exec eslint src/__tests__/project-tree-archive-race.test.ts; pnpm exec tsc --noEmit -p tsconfig.test.json; go run ./tools/repolint; git diff --check.
* Fix duplicate compaction admission and session receipts
Problem: Repeated /compact could show a failed chat bubble and leave stale running state after switching sessions.
Root cause: Management preflight ran outside tab admission, display submissions bypassed typed receipts, and management requests entered the optimistic chat lifecycle.
Fix: Correlate maintenance conflicts under the tab lock, route compact forms through typed admission, and keep management receipts separate from conversational turns while preserving queued follow-up targeting. Add deterministic browser coverage to PR CI.
Verification: Focused Desktop tests and race tests, frontend regressions and browser tests, transcript tests, Go vet, repolint, and bundle gates passed locally. On main-v2, initial raw assets measure 2099087 B before and 2099980 B after (+893 B); the narrowly rounded 2051.0 KiB gate preserves the observed 186 B Linux producer difference.
* Fix maintenance browser runtime lookup in CI
Problem: The transcript browser job could not launch Chromium after the new maintenance regression was added.
Root cause: Playwright downloaded to frontend/.pw-browsers, but a relative environment override resolved from the desktop shell directory.
Fix: Let the maintenance fixture use its frontend-root browser path and document that ownership in the CI step.
Verification: The exact pnpm --dir frontend test:maintenance-browser command passed from desktop with a local frontend browser runtime; workflow YAML parsed and diff checks passed.
Problem: the expanded Desktop CI matrix rejected four snapshot lint findings and the test TypeScript build could not observe the recovery marker added by the project-tree window loader.
Root cause: the Desktop module has independent lint gates, while the generic loader return type preserved only the inferred callback shape even though recovery adds replacedSnapshot.
Fix: apply the lint-safe equivalents, remove the superseded wall-clock helper, and expose the optional recovery marker in the loader return contract.
Verification: cd desktop && go test ./...; golangci-lint run --timeout=5m; GOOS=windows golangci-lint run --timeout=5m ./...; pnpm exec tsc --noEmit -p tsconfig.test.json; pnpm typecheck; pnpm exec eslint src/lib/projectTreeWindow.ts; pnpm exec tsx src/__tests__/read-snapshot-window.test.ts; go run ./tools/repolint; git diff --check.
* fix(config): refresh official DeepSeek model catalogs
Problem: New official connections offered retired Flash model IDs, while
existing official connections lacked the current deepseek-flash choice.
Root cause: The three protocol presets and runtime backfills had independent
legacy catalogs, and saved model lists were never upgraded once.
Fix: Use the current Flash/Pro catalog for new connections. Migrate existing
official lists once under the config edit lock, preserving selections,
user-managed deletions, historical model references, prices, and unknown TOML.
Verification: Config, boot, CLI, provider, Desktop, frontend contract and
browser checks; interrupted-write and previous-reader round trips; repolint.
* docs: describe canonical DeepSeek Flash catalogs
Problem: The user guides and TOML examples still presented retired Flash IDs
as selectable defaults and described current Flash as text-only.
Root cause: The catalog and vision changes had not been reflected in the
English and Chinese documentation.
Fix: Update new-connection examples, vision behavior, legacy-reference
compatibility, and the one-time version 11 catalog upgrade.
Verification: Reviewed paired English and Chinese sections and ran the
documentation impact guard on the updated PR body.
* test(desktop): align official DeepSeek catalog expectations
Problem: Desktop CI still expected retired official Flash and vision options after the catalog upgrade, and the protocol-upgrade fixture used a new connection that is no longer eligible.
Fix: Assert the current selectable models and telemetry bucket while preserving legacy defaults, and exercise protocol upgrade with an actual legacy single-model connection.
Verification: Focused Desktop Go tests covering all seven failing cases pass locally; git diff --check passes.
Problem: Linux CI rejected unchecked read-transaction rollbacks, and the Node 24 frontend build exceeded the raw initial bundle ceiling by 84 bytes.
Root cause: the new snapshot read views used deferred Rollback calls without an explicit ignored result, while the bundle ratchet only reflected the smaller local Node 26 producer output.
Fix: explicitly discard the expected rollback result and move only the raw initial asset ceiling to the next measured 0.1 KiB step. All compressed, chunk, CSS, locale, and resident-history limits remain unchanged.
Verification: go test ./internal/historycatalog ./internal/sessioncatalog; go run ./tools/repolint; golangci-lint run --timeout=5m; pnpm typecheck; node scripts/check-bundle-budget.mjs; git diff --check.
Remove a duplicate project-tree key helper and record the measured 698-byte startup payload increase from immutable snapshot pagination. Keep gzip, chunk, CSS, locale, and resident-history limits unchanged.
Problem: snapshot pagination orchestration exceeded repository ownership limits in already-large components and functions.\n\nRoot cause: presentation, catalog search capture, snapshot build publication, and metadata paging remained embedded in their callers.\n\nFix: extract those responsibilities without changing cursor, generation, release, or migration semantics.\n\nVerification: repolint, focused Go and frontend tests, typecheck, ESLint, and focused race tests.
* feat(desktop): simplify archived conversation management
Problem: Archive management repeated page headings and placed restore, delete, refresh, and clear actions together on every row.
Root cause: The management page and archived list each owned overlapping chrome, while row actions had no preview-centered hierarchy.
Fix: Use one archived page heading and a split list/preview layout. Move bulk actions to the existing menu, keep single-session actions with the read-only preview, and retain lifecycle generation, retry, and legacy placeholder behavior. Make bulk confirmation explicit even with one entry and bound dialog height on short windows.
Verification: Focused archive lifecycle, confirmation generation, placeholder, pagination, and bulk-scope tests; frontend typecheck and lint; desktop browser layout and interaction checks.
* docs(desktop): update archive management guide
Problem: The management guide still described an older archive layout and filtering model.
Fix: Document the split preview, menu actions, responsive breakpoint, and full-scope clear confirmation.
Verification: Reviewed against the implemented archive page and updated the PR documentation-impact declaration.
* Restore complete transcript turn navigation
Problem: Follow v2 limited the turn rail to the bounded resident transcript window, so long conversations lost direct access to unloaded turns.
Root cause: the rail depended on runtime projection state and reused sequential history paging for jumps instead of reading a durable directory and locating by stable message identity.
Fix: add a paged durable history outline for local and remote sessions, sparse frontend caching, direct target-window loading, cancellable scroll transactions, compatibility handling, and full interaction documentation.
Verification: root and Desktop Go tests, focused race tests, frontend typecheck/lint/transcript suites, production build budgets, and 25,000-turn Chromium/Electron interaction fixtures.
* Fix history outline lint findings
Problem: the first PR head failed lint-code on unchecked rollback results, loop string concatenation, and numeric HTTP status literals.
Root cause: the new outline implementation and tests did not follow the repository golangci rules enforced in CI.
Fix: explicitly ignore rollback cleanup errors, use strings.Builder for query-plan assembly, and use http.StatusOK.
Verification: golangci-lint run --timeout=5m; go test ./internal/session ./internal/serve -run TestHistoryOutline; go run ./tools/repolint.
* Refresh Desktop migration inventory
Problem: CI detected stale Desktop migration inventory after adding the local and remote history outline commands.
Root cause: the generated inventory was not refreshed after the new App methods changed the command set and source locations.
Fix: regenerate both inventory artifacts with tools/desktopinventory.
Verification: go test ./tools/desktopinventory; go run ./tools/repolint; git diff --check.
* Stabilize legacy turn rail identities
Problem: the transcript browser CI reported duplicate React keys during optimistic-to-persisted submission handoff.
Root cause: hosts without history-outline-v1 only guarantee resident turn order, while transient projections can reuse an absolute history turn number; the fallback rail used that number as its React identity.
Fix: number the loaded-only fallback rail by resident order while leaving persistent outline ordinals unchanged.
Verification: pnpm test:submission-browser; pnpm test:submission-electron; pnpm test:transcript; pnpm test:typecheck; pnpm lint:hooks; pnpm check:scroll-writer; pnpm build; go run ./tools/repolint.
* Stabilize transcript async wait budget
Problem: Session, project and history lists could report stale_cursor or mix rows while metadata changed, background refreshes ran, or users switched between projects and sessions.
Root cause: Pagination validated mutable live metadata and several UI paths reused cursors, request generations and preview state across logical read identities.
Fix: Add bounded immutable read snapshots with lifecycle fences, consistent catalog read views, whole-window frontend staging and recovery, explicit snapshot release, and identity-scoped switch handling across project and history lists.
Verification: Focused Desktop and catalog tests, deterministic A-B-A switching regressions, race tests, frontend typecheck/lint, Chromium independent-session coverage and Windows cross-compilation passed. The broad Desktop suite remains limited by a pre-existing transcript performance hang and total runtime.
* fix(windows): restore Git Bash and shell settings
Problem:
- Windows settings no longer exposed the earlier shell layout or Git Bash runtime.
- WSL aliases and retained PowerShell paths could misroute interpreter selection, while custom runtimes could be absent from detection.
- ConPTY could corrupt Git Bash UTF-8 output and completion markers.
Root cause:
- Shell resolution and capability inventory applied different path compatibility rules.
- Machine-driven Windows Bash sessions reused a terminal-oriented transport.
- The settings presentation had diverged from the runtime state model.
Fix:
- Restore Git Bash discovery, selection, and the compact sandbox settings surface.
- Centralize configured-path compatibility, reject WSL aliases, and align inventory with resolution.
- Use a tracked persistent pipe for machine-driven Git Bash sessions and add Windows contract coverage.
Verification:
- go test ./...
- go test -race ./internal/persistentshell ./internal/sandbox -count=1
- focused desktop shell support tests
- frontend typechecks, lint, CSS and application-layer checks, and 154 focused assertions
- native Windows 11 ARM64 shell and integration regressions
* chore(frontend): calibrate sandbox bundle budget
Problem:
- The restored localized sandbox labels exceeded the raw startup budget by 1606 bytes.
Root cause:
- The raw budget was calibrated before the new runtime, status, and write-scope labels entered the startup locale payload.
Fix:
- Ratchet only the raw initial asset budget to the measured payload with bounded headroom.
- Keep compressed, chunk, stylesheet, locale, and resident-history limits unchanged.
Verification:
- pnpm build
Problem:
Built-in browser tools could be discovered as ready but fail at execution
after a desktop runtime detached, reattached, or moved to another surface.
Root cause:
Long-lived browser executors captured the surface ID used at boot instead of
following the runtime-owned event sink. Capability discovery also read the
stable catalog without applying the live execution-context gate.
Fix:
Resolve browser grants through the runtime binding, revoke grants safely on
surface transfer and cleanup, and pass candidate sinks through every rebuild
path. Share contextual availability diagnostics between discovery and
execution while keeping the stable catalog and provider tool schemas intact.
Verification:
Added deterministic detach, reattach, replacement, cleanup, preview, and
concurrent-transfer regressions plus discovery/execution consistency tests.
Root and Desktop suites, focused race tests, vet, and the Windows amd64
cross-compile pass.
Problem: Large historical catalogs and concurrent sessions amplify CPU,
allocation, rendering, and layout persistence work during navigation.
Root cause: Settled reads repeat registry decoding and write transactions,
unchanged discovery invalidates catalogs, and runtime events resample unrelated
controllers. Evicted transcript entries can retain controller references.
Fix: Cache validated metadata and bounded topic orders, keep display reads
separate from operation journals, coalesce global runtime updates and layout
writes, isolate background rendering, release evicted projections, and refresh
dirty catalog directories with periodic fallback checks.
Integration: Preserve canonical transcript structural reconciliation during body
reclamation and derive metadata cache paths from confined storage. Calibrate
the raw bundle ratchet using the measured 1551-byte increment.
Verification: Root Go packages and all seven inventory-checked Desktop test
groups passed, with focused race tests, transcript and navigation regressions,
TypeScript checks, lint, Chromium interaction checks, and Windows cross-build.
Native Windows CPU reduction has not been measured.
Co-authored-by: f1shsmell <[email protected]>
* fix(release): preflight executable contracts and recover site independently
Problem: late public-access failures required full release recovery and could leave incomplete site evidence. Root cause: static control checks did not execute critical contracts, while website reconciliation lived only inside publication YAML. Fix: check offline contracts and runner manifest access early, centralize public-state reconciliation, and add protected site-only recovery with a shared lock, owned Pages correlation, final ownership check, and persistent failure ledgers. Verification: executable preflight, shell-boundary failure regressions, release workflow contracts, CI path tests, and actionlint passed without live publication.
* fix(release): serialize legacy and site recovery
Problem: legacy recovery could overlap site reconciliation. Root cause: its lock was scoped per version instead of sharing the publication lock. Fix: join the common stable-release-publication concurrency group. Verification: workflow lint and site recovery lock regressions passed.
* fix(release): bind atomic tag activation to an authorized publisher
Problem: release approval could precede a GH013 tag-creation failure. Root cause: activation inherited the default Actions token rather than an explicitly authorized maintainer identity. Fix: require a dedicated credential, inspect inherited rulesets before approval, bind the actor ID, and recheck before the existing atomic push without checkout-token fallback. Verification: identity CLI and policy regressions, local atomic tag tests, release workflow contracts, CI path tests, and actionlint passed.
* test(release): assert the dedicated tag credential boundary
Problem: the old workflow regression required a writable default Actions token during activation. Root cause: the assertion encoded the retired credential implementation rather than the approval and identity boundary. Fix: require read-only default permissions, explicit publisher credentials, actor binding, no checkout fallback, and activation after approval. Verification: CI workflow and identity regressions passed together.
Sparse completion events and later history hydration could erase durable tool identity, append duplicate result rows after the final answer, and downgrade completed state. Preserve canonical metadata, repair uniquely matchable legacy checkpoint identities, and reconcile bounded resident records through one conflict-aware projection with stable tool keys and anchored local notices.
Keep distinct conflicting formal results independently readable and prevent old session requests from updating a new binding. Preserve pagination, content-on-demand and existing resident-memory limits without changing provider payloads, prompts, session exports or the persistent message format.
Harden CI at the observed failure boundaries: carry frontend producer attempts into artifact consumers, attribute actual bounded Markdown Worker listeners, synchronize reconnect assertions with retained lifecycle publications, and use a fresh bounded functional wait for packaged upgrade history. Retain native geometry, visible history, restart integrity and independent startup gates.
Verification includes transcript/store/follower/reducer regressions, 240- and 1,000-turn browser scenarios, native Electron layout, backend and race suites, packaged Windows first-import/restart acceptance, and all three full memory-screening shards. Real remote-host validation remains unavailable; binding tests do not replace it. No release is published by this change.
Problem: Windows checkpoint CI passes the durable replay assertions but fails waiting five seconds for its unrelated cleanup TurnDone event.
Root cause: A reload-while-writing test adds a cancellation latency contract after its actual invariant has already been verified.
Fix: Use the owned test controller teardown to cancel and join all workers and stores. Remove only the unrelated timing assertion and duplicate cleanup; retain completed-tool and active-writer replay assertions.
Verification: The checkpoint regression and cancellation tests pass three race-enabled iterations. Dedicated cancellation tests remain intact.
Problem: Ubuntu CI fails before the steer ordering assertion because provider admission exceeds an unrelated one-second startup assumption.
Root cause: Durable inbox and lifecycle checkpoint I/O runs on shared CI storage; these correctness tests accidentally impose a latency SLA.
Fix: Share the existing bounded inbox-dispatch wait and state diagnostics across both steer-provider tests, and always close the controller before joining autosave cleanup. Keep durable ordering and exactly-once assertions unchanged.
Verification: Both affected regressions pass 10 race-enabled iterations; no production timeout or behavior changes.
Problem: Superseded runs retain queued always() aggregates and metrics, blocking replacement CI until manually force-cancelled.
Root cause: Unconditional aggregation survives workflow-level cancellation even after all useful worker jobs exit.
Fix: Gate aggregates and metrics on !cancelled(). This explicit status function still runs against failed dependencies, preserving fail-closed result assertions for live workflows.
Verification: 35 CI workflow/path contract tests and actionlint pass, covering cancelled aggregation and failed dependency execution.
Problem: Windows layout CI times out taking its final screenshot after all 32 geometry assertions pass.
Root cause: CDP capture waits for a compositor frame from the intentionally hidden native fixture window.
Fix: Use Electron webContents.capturePage for native fixtures and reject empty images. Preserve Chromium screenshot capture, all geometry assertions, and evidence output without showing the window.
Verification: Native Electron layout suite passes all 32 scenarios and writes a nonempty 2560x1800 PNG; diff checks pass.
Problem: Desktop preparation rejects the integrated payload by two bytes.
Root cause: The Linux Electron producer emits 2,084,968 bytes while the previous one-decimal ceiling allows 2,084,966 bytes.
Fix: Document the measured platform result and retain the next tenth of a KiB, leaving 100 bytes of headroom. All gzip, per-chunk, CSS, locale, and structural gates remain unchanged.
Verification: Production build and bundle checks pass locally after sharing maintenance lookup and classification logic; the exact failing CI measurement establishes the bounded producer ceiling.
Problem: New main-v2 changes conflict with generated Desktop contracts and inventory.
Root cause: Both branches extend the shared host and transcript surfaces.
Fix: Regenerate contracts and inventory from combined sources, retain both behaviors, and share pending-compaction lookup and state classification without increasing bundle gates.
Verification: Affected root and Desktop suites, generated inventory, frontend production build, test typecheck, transcript suite, and Chromium transcript interaction/performance checks passed.
Problem: CI can time out before the shutdown fixture reaches saving, despite the detached timeout returning the correct current phase.
Root cause: a 250ms deadline included earlier lifecycle checkpoint I/O, so the assertion depended on runner and filesystem speed.
Fix: start the shutdown transaction, wait for the blocked-save channel, then exercise the existing bounded detached waiter against that same transaction. Retain the 250ms timeout and interrupted-evidence assertions; always release and join the worker during cleanup.
Verification: the exact regression passes 10 iterations under the race detector, Desktop golangci-lint 2.12.2 passes, and repolint remains clean. Production shutdown behavior is unchanged.
Integrate current main-v2 without changing compaction behavior. Resolve the generated inventory overlap by rebuilding it from the combined source tree rather than selecting either stale copy.
Verification: desktop inventory classification and freshness tests pass; no compaction implementation files conflicted.
Problem: CI lint rejects legacy integer and reverse slice loops, and Desktop lint reports an unused capability constant.
Root cause: new maintenance helpers did not use the repository-pinned Go iteration conventions; the remote capability declaration had no consumer.
Fix: use integer range and slices.Backward while preserving traversal and update order, and remove the unused declaration. Server capability advertisement is unchanged.
Verification: golangci-lint 2.12.2 passes in root and Desktop with isolated caches; targeted agent/control and Desktop maintenance tests pass; repolint remains clean.
Problem: Switching conversations briefly replaced the transcript with duplicated history-loading and recovery surfaces, causing a visible flash for fast loads.
Root cause: Normal hydration reused the error-recovery presentation and exposed loading feedback immediately across local, remote, and transcript surfaces.
Fix: Centralize staged loading feedback with a 250 ms grace period, preserve cached transcript content, reserve recovery UI for actual failures, and fence timers by session identity.
Verification: pnpm build; pnpm test:session-loading-browser; focused session loading/recovery and tab-switch tests; pnpm test:transcript; pnpm test:transcript-browser.
Problem: the current combined tree introduces the upstream historical recovery writer.lock persistence entry, leaving the checked-in inventory stale.
Root cause: generated inventory predates the merged recovery implementation.
Fix: regenerate the classified inventory and source locations; record the final extracted frontend payload measurement without changing its budget.
Verification: the final root suite passed every package except the stale inventory guard; go test ./tools/desktopinventory passes after regeneration. Frontend assets and all existing budget limits pass.
Use errors.Is for the file preview takeover assertion so Desktop lint accepts wrapped sentinel errors on every target.
Verification: focused file browser preview tests; desktop golangci-lint on the host and GOOS=windows; repolint.
Problem: public manifest failures lacked endpoint diagnostics, and base Desktop inventory had drifted.
Root cause: HTTP errors were terse and recent Desktop source changes were not reflected in generated indexes.
Fix: report selected public response headers on failure and regenerate the inventory without changing Desktop behavior.
Verification: executable observer regressions and desktopinventory tests pass.
Preserve upstream native service shutdown and historical-session recovery behavior. Resolve the startup budget overlap using the measured 2084732-byte combined payload and the existing 186-byte Linux producer allowance; other budget gates remain unchanged.
Verification: merged Desktop maintenance/history/contract tests, frontend test typecheck, transcript suite, build static gates and Vite build passed. The measured combined bundle passes the reconciled budget.
Problem: manual compaction could appear stuck, ignore Stop, lose its outcome after reload, or overlap other foreground work. Automatic recovery could install a projection after cancellation.
Root cause: maintenance bypassed shared runtime ownership and turn-independent persistence; client history and snapshots lacked consistent operation ordering.
Fix: centralize maintenance admission, cancellation, commit fencing, durable lifecycle records, and queue handoff. Reconcile versioned operation cards across desktop, remote, and CLI; classify empty history safely and preserve recovery details.
Verification: root and Desktop Go suites, deterministic maintenance regressions, focused Go race tests, frontend typechecks, transcript tests, production build, contract checks, and Chromium interaction/scroll benchmarks passed. Updated English and Chinese behavior documentation.
Problem: recovery could skip Pages and report success after an HTTP 403.
Root cause: a curl pipeline hid the failed read, and any unequal pointer was treated as newer.
Fix: share a checked manifest observation and require a strictly newer stable version before preserving the site.
Verification: executable HTTP-error and version-order regressions, publication ledger tests, and full release workflow contracts pass.
The responsive workspace close action reused the add-tab class, so strict browser selectors matched both controls.
Introduce a shared visual action class while preserving unique add and collapse classes.
Verification: pnpm build; pnpm test:app-browser; pnpm typecheck; pnpm test:workspace.
Bring the PR head onto the current main-v2 after historical version recovery changed Desktop inventory inputs.
Regenerate the Desktop migration inventory so the merged tree satisfies its freshness guard.
Verification: go test ./tools/desktopinventory ./internal/browser ./internal/boot; go test ./internal/workspacestate; focused desktop historical and browser preview tests; pnpm test:workspace; go run ./tools/repolint.
The workspace resize test now reaches the lazily loaded code viewer, whose syntax stylesheet is a browser asset. Run that authoritative package-script entrypoint with the existing CSS stub loader so Node CI preserves the real component dependency without trying to execute CSS.\n\nVerification: pnpm test:workspace, CI plan inspection, and repolint passed.
Extract file preview request validation so the shared browser preview path stays within the repository complexity budget. Keep workspace HTML routing concise enough to avoid growing the existing oversized panel baseline.\n\nVerification: repolint, frontend typecheck and ESLint, and focused Desktop preview/browser tests passed.
Problem: The right dock opened at a fixed width and narrow-window presentation did not consistently accommodate browser previews.
Root cause: Dock preferences and constrained widths were mixed during restoration, and responsive styles were loaded only with the file panel.
Fix: Initialize new dock widths at 45 percent of the viewport, preserve saved preferences, protect chat space, and cap split widths at 70 percent. Add responsive navigation collapse and full-width dock presentation below 768px, shared panel styles, and a panel-close action.
Verification: Typechecks, lint, CSS checks, production build and unchanged bundle budgets passed. Layout and browser surface tests, transcript regressions, and Chromium and isolated Electron responsive dock checks passed, including pointer and keyboard resizing, terminal coexistence, reopening and reload restoration.
Problem: Chat code fences lacked a persistent language toolbar, theme-aware semantic colors, and stable highlighting while streamed content grew.
Root cause: Code block controls lived inside the source surface, streaming fence tails bypassed the highlighted viewer, and syntax styles were loaded in the initial app stylesheet.
Fix: Add a framed Markdown code viewer with language and copy actions, contrast-corrected semantic palettes, retained highlighted line hosts, and a shared streaming fence path. Lazy-load code block and syntax styles with the viewer.
Verification: Frontend build and bundle budgets, typechecks, transcript and Chromium regressions, focused Markdown and theme tests, and 33 browser theme cases passed; minimum measured contrast was 4.87:1.
Problem: HTML file entry points could open externally or target tabs inaccessible to the current agent. Deliverable menus were clipped and changed-file summaries could misreport net edits.
Root cause: navigation, preview ownership and resource lifecycle were split across UI and executor paths. Card menus lived inside the transcript scroller and fallback counts were derived from mutation calls.
Fix: share authorized task/session-bound file previews, expose on-demand browser_preview, route HTML internally, and refresh/release preview resources. Add frozen-diff summaries and split file actions, portal menus, accurate incomplete statistics, and explicit replay-page limitations.
Verification: frontend typechecks, transcript and focused workspace tests, Chromium transcript regression, root browser/boot tests, focused Desktop Go preview/browser tests, Electron typecheck and grant/host-call tests passed. Full native HTML/agent end-to-end acceptance remains unverified.
Problem: publication receives an empty source SHA after provenance checks. Root cause: generic camel-case conversion splits SHA into s_h_a, violating consumer output names. Fix: explicitly map all workflow fields and reject malformed source/control identities. Verification: real CLI GITHUB_OUTPUT regressions cover publish and rehearsal, fail with the original conversion, and pass with the repair; candidate and release workflow contracts pass.
Problem:
The Desktop Linux and cross-platform lint jobs rejected the historical-version reservation scan.
Root cause:
The new lookup manually iterated over a session ID slice, which the pinned modernize linter flags when slices.Contains expresses the same invariant.
Fix:
Use slices.Contains for the reservation membership check without changing conflict behavior.
Verification:
Desktop and Windows golangci-lint, workspace-state and historical-version tests, the focused race test, and repolint passed.
Co-authored-by: Fucha <[email protected]>
Problem:
Historical source updates could publish content but fail registry adoption, while the UI swallowed terminal failures and appeared inert.
Root cause:
Ordinary and versioned imports reused operation identity, restart replay lost version identity, and target recovery searched the unversioned source key.
Fix:
Give version imports distinct compatible identities, recover exact reservations, validate and atomically adopt safe content-ready targets as version branches, preserve the original mapping, and surface progress and errors in the banner.
Verification:
Desktop migration/recovery/workspace regressions, race tests, frontend interaction and type checks, browser bench coverage, and Windows amd64 GUI cross-build passed.
Co-authored-by: Fucha <[email protected]>
Problem:
The Windows packaged smoke timed out even though the shell log showed that the service had reached ready.
Root cause:
The new build-correlation fields changed the stable ready prefix from 'generation X, pid Y' to equals-delimited fields, so the packaged smoke parser never recognized readiness.
Fix:
Restore the existing generation and pid prefix, append version/channel/commit after it, and centralize the parser with a regression that accepts added fields while rejecting incompatible delimiters.
Verification:
- node --test desktop/packaging/smoke-poll.test.mjs
- cd desktop/electron && pnpm typecheck && pnpm test
- go run ./tools/repolint
- git diff --check
Problem:
The app-memory CI build loaded the transcript diagnostic publisher into the startup bundle and exceeded the bounded raw asset budget.
Root cause:
Crash formatting imported the publisher module only to read its current summary, which made the full publish path eager. Repeated native host adapters also added avoidable startup code.
Fix:
Store the bounded active transcript summary on the renderer global, read it without a static module edge, reuse formatted diagnostic strings, and share stable native host adapters and window error handling.
Verification:
- REASONIX_APP_MEMORY_PROFILE=full pnpm build
- pnpm typecheck
- pnpm test:typecheck
- pnpm test:window-state
- transcript-follow-client.test.ts
- windows-maximised-sync.test.tsx
- crash-reporting.test.ts
- go run ./tools/repolint
- git diff --check
Problem: Adding transcript context to the crash payload pushed crash.ts beyond its repository file-size allowance.
Root cause: The crash module duplicated access to the diagnostic snapshot and expanded formatting across additional local helpers.
Fix: Export the bounded snapshot reader from transcriptDiagnostics and keep the crash payload integration compact without changing its visible content.
Verification: Repolint is clean, frontend source and test typechecks pass, and all 22 transcript-follow diagnostics regressions pass.
Problem: The follow-up race review and its exact evidence were not recorded with the Windows validation boundary.
Root cause: The original note predated the additional preparation, remote-follower, generation-cleanup, and diagnostic-aggregation regressions.
Fix: Record the repaired invariants and local verification matrix in both validation documents.
Verification: English and Chinese notes agree, links resolve, and repository diff checks pass.
Problem: A quit during renderer resume or a draft-error dialog could strand shutdown in preparing, while remote or late transcript followers could retry and clean up after service stopping.
Root cause: Preparation released phase ownership before awaited renderer work completed, stop fencing lived only in the local controller map, cleanup policy was not generation-owned, and snapshot replacement was misclassified as recovery.
Fix: Retain preparation ownership across awaits, replay queued exit intent, make every follower observe service state, bind cleanup policy to its generation, require a successful delta before recovery, and make diagnostics capability-safe.
Verification: Deferred and fake-clock regressions cover quit, relaunch, dialogs, local and remote followers, late baselines, retries, cleanup ownership, diagnostic compatibility, and breadcrumb aggregation; all focused suites pass.
Problem: Clean exits can remove lifecycle files, making an empty diagnostics directory easy to misread as missing instrumentation.
Root cause: The durable shell-log boundary, native Windows qualification steps, and migration scope exclusion were not documented together.
Fix: Document shell-log correlation fields, expected lifecycle cleanup, the Windows package matrix, and the separate migration investigation model in English and Chinese.
Verification: Documentation links and repository diff checks pass; native Windows package qualification remains explicitly outstanding.
Problem: Transcript retry logs exposed only a final error string and could disappear with lifecycle evidence after a clean exit.
Root cause: Failure stage and reason were not captured at their owning validation sites, and renderer diagnostics depended on the Go service remaining callable.
Fix: Add stable transcript failure classifications, bounded aggregation and recovery records, crash summaries, and a validated rate-limited renderer-to-shell diagnostic channel.
Verification: Local and remote transport, protocol, snapshot, delta, deduplication, recovery, shutdown, typecheck, transcript, and production-build checks pass.
Problem: Repeated Windows title-bar closes could call the Go service after shutdown began, yielding a ready-state contradiction and an unhandled renderer rejection.
Root cause: Native close events, renderer window commands, and app quit maintained overlapping shutdown transactions while the service did not publish stopping at the shutdown boundary.
Fix: Route native window commands to Electron, centralize close and quit sequencing, coalesce preparation and shutdown, and publish stopping before rejecting business RPCs.
Verification: Electron lifecycle, IPC, service, typecheck, and build gates pass with deterministic repeated-close, quit-upgrade, save-failure, restart, and stopping-state regressions.
Problem: Windows shutdown tests infer cancellation failure from a 750 ms total teardown budget. Root cause: unrelated persistence and cleanup share that wall-clock assertion. Fix: observe the cancellation channel, separately join teardown and the cancelled open, preserve the blocked-build ordering check, and bound failure cleanup. Verification: both focused tests pass ten repetitions and race detection; formatting and diff checks pass.
Problem: independent Windows signing fails before installer assembly. Root cause: finalization deletes tracked NSIS inputs and packaging assumes a native build created its output directory. Fix: preserve committed packaging sources and initialize generated output at the packager boundary. Verification: fresh-checkout finalization covers amd64 and arm64, both original defects fail the new regression, all release contracts and diff checks pass.
Problem: live write qualification called retired recovery mutation APIs. Root cause: its helper retained the old confirmation contract after history became immutable. Fix: continue from externally verified file state and assert the historical record stays byte-identical, while retaining no-repeat and checkpoint checks. Verification: live Chat, Responses, and Anthropic cases pass; retired recovery deterministic regression and diff checks pass.
Problem: candidate preparation fails before jobs start. Root cause: GitHub validates permissions of skipped nested publisher jobs against the read-only caller. Fix: grant the required permission union at the Desktop call and validate all candidate and publication callers. Verification: regression rejects original workflow; release workflow contracts, actionlint, and diff checks pass.
Clarify repaired image attachment support, model reasoning effort, and Windows PowerShell behavior. Reviewed the complete first-parent release range and linked documentation. Verification: release catalog validation, release-notes tests, and git diff --check.
Summary:
Generate a bilingual, product-focused draft from merged pull request metadata. Reuse the selected release-bound PR when one is available.
Verification:
Validate the catalog, citations, bilingual fields, and rendered GitHub release notes before committing.
Problem:
The native Windows Bot package intermittently exited with 0xc0000005 or a bare package failure during stop and cancellation tests.
Root cause:
Several Bot test controllers embedded a nil interface. Normal gateway calls intentionally dereferenced that interface and production wrappers recovered the panic. On Windows Server 2025 hosts with large CPU XSTATE, dispatching the hardware exception can corrupt the Go heap (golang/go#81238), so recover cannot make this test pattern safe. Removing the recover also exposed that a controller panic while the gateway lock was held could wedge later messages.
Fix:
Use a complete inert controller stub in tests, stop recovering controller method panics in production, and move the session claim decision behind a defer-unlocked boundary. Record the isolated Windows runner CPU and include Bot in the PR race suite.
Verification:
- go test -count=5 -timeout=5m ./internal/bot/...
- go test -race -timeout=10m ./internal/bot/...
- go vet ./internal/bot/...
- node --test scripts/ci-workflow.test.mjs scripts/windows-go-tests.test.mjs
- go run ./tools/repolint
- git diff --check
Problem:
The isolated Windows Bot package can terminate at process level with only a package FAIL line, leaving no test name or final event for root-cause analysis.
Root cause:
The Bot lane used the default go test text stream, which omits enough subprocess event detail when the test binary exits without a Go panic stack.
Fix:
Emit go test JSON for the existing single-process Bot lane and lock that diagnostic contract in the Windows selector test. This does not retry, split, or skip the package.
Verification:
- node --test scripts/windows-go-tests.test.mjs scripts/ci-workflow.test.mjs
- go run ./tools/repolint
- git diff --check
Problem:
The Windows Desktop I-P partition could report that an A-to-B rebind never restored its candidate while legacy-session migration was still retrying a transient Access denied rename.
Root cause:
The regression test waited on only the restored hook for ten seconds and ignored both the earlier built phase and an early rebind error, conflating migration latency with a stuck transaction.
Fix:
Observe candidate construction and restoration as separate ordered events, surface early rebind errors immediately, give the measured Windows restore phase its own hang guard, and always release the commit hook on failure.
Verification:
- go test -timeout=8m ./... -run TestModeRebuildAndABANavigationFenceLeaseFailureAndOldEpochEvent -count=20
- go test -race -timeout=8m ./... -run TestModeRebuildAndABANavigationFenceLeaseFailureAndOldEpochEvent -count=5
- go run ./tools/repolint
Problem:
The Windows PR contract could time out after a valid permission response while the loaded runner was completing the approved tool turn.
Root cause:
The test used one idle timer for tool execution, status publication, and transcript persistence, so it could not distinguish a broken approval round trip from delayed post-tool finalization.
Fix:
Observe the approved tool execution explicitly, then apply a separate bounded finalization wait. Keep the shared prompt drain limit unchanged for other tests.
Verification:
- go test -timeout=5m ./internal/acp -run TestE2EApprovalRoundTrip\|TestE2ECancelMidTurn -count=20
- go test -race -timeout=8m ./internal/acp -run TestE2EApprovalRoundTrip\|TestE2ECancelMidTurn -count=5
- go run ./tools/repolint
Problem:
The Windows PR contract occasionally failed TestE2ECancelMidTurn after the tool had started because the test required all post-cancel finalization to finish within two seconds.
Root cause:
The fixture observed tool start but not cancellation delivery, so scheduler and transcript-finalization latency were conflated with a broken cancellation path.
Fix:
Signal when the blocking tool observes context cancellation, assert that event before waiting for the prompt result, and retain an independent hang guard for finalization.
Verification:
- go test -timeout=4m ./internal/acp -run TestE2EApprovalRoundTrip\|TestE2ECancelMidTurn -count=10
- go test -race -timeout=6m ./internal/acp -run TestE2ECancelMidTurn -count=5
- go run ./tools/repolint
Problem:\nCodeQL still followed the validated request string into storage because syntactic validation alone does not establish server ownership of an identity.\n\nRoot cause:\nThe export resolver constructed SessionRef directly from the caller-selected ID, even though persistence later confined the path.\n\nFix:\nResolve explicit IDs by equality against paged server-side catalog entries and construct SessionRef only from the catalog-owned value. The request string is never passed to a filesystem-backed read.\n\nVerification:\ngo test -timeout=8m ./internal/session -run ResolveSessionID -count=1\ngo test -timeout=8m ./internal/serve -run SessionExport -count=1\ngo run ./tools/repolint
Problem:\nCodeQL traced the new explicit sessionId request values into session storage path operations, so the fixed-source endpoint did not make its path boundary locally auditable.\n\nRoot cause:\nThe session query validated IDs only after the HTTP resolver had propagated the caller-owned string into a SessionRef.\n\nFix:\nApply the canonical cross-platform SessionID validator at the shared export resolver, reduce accepted identities to one filepath.Base component, reject malformed query/header/body targets with 400, and cover traversal-shaped inputs.\n\nVerification:\ngo test -timeout=8m ./internal/serve -run SessionExport -count=1\ngo run ./tools/repolint
Problem:\nThe full native Windows contract still dereferenced the retired bash registration, expected OS-sandbox denial tokens on an unsupported platform, and repolint rejected the cold diagnostic serializer.\n\nRoot cause:\nWorkspace tests bypassed the platform shell contract, the denial retry test did not separate OS-sandbox capabilities, and commit streaming lived inside the top-level serializer.\n\nFix:\nDiscover and assert the single platform shell, limit exact denial retry coverage to supported OS sandboxes, and extract cold diagnostic commit streaming without changing its format.\n\nVerification:\ngo run ./tools/repolint\ngo test ./internal/control ./internal/tool/builtin\nnode --test scripts/windows-go-tests.test.mjs scripts/ci-paths.test.mjs\nGOOS=windows GOARCH=amd64 CGO_ENABLED=0 go test -c for builtin, control, ACP, and Bot
Problem:
ACP cancellation and approval tests reported a generic timeout when the prompt had already failed, and a two-second scheduling deadline was unstable in full Windows package runs.
Root cause:
The fixture waited for only one expected event and used elapsed time as the correctness assertion, hiding early RPC failures and conflating scheduler load with protocol ordering.
Fix:
Wait concurrently for tool or approval entry and the prompt response, report early RPC completion explicitly, and retain a separate ten-second hang guard. Native Windows CI now runs ACP and Bot as isolated whole packages so any remaining Bot process fault keeps its original signature.
Verification:
Focused ACP tests pass repeatedly and under the race detector; the package cross-compiles for windows/amd64. Native Windows CI remains the required evidence for the prior Bot access violation.
Problem:
Windows PR checks asserted retired sandbox and POSIX shell assumptions, and ACP/Bot failures were mixed into a parallel residual package lane.
Root cause:
The focused selector did not follow platform shell registration, current schema requirements, unsupported OS sandbox behavior, or the packages that own lifecycle cancellation.
Fix:
Resolve the registered primary shell in tests, assert platform-specific schema, split supported and unsupported sandbox contracts, centralize Windows PR selections with coverage assertions, run the full builtin package for shell/permission ownership changes, and isolate ACP and Bot package execution. Defender setup now fails closed so native crash evidence has a known prerequisite.
Verification:
Selector and workflow contract suites pass, builtin tests pass on macOS, and builtin, ACP, and Bot packages cross-compile for windows/amd64.
Problem:
A read-only export of session A failed or drifted after the remote foreground switched, the session was taken over, or diagnostics lost their live controller.
Root cause:
Export endpoints revalidated against the current foreground binding instead of resolving the immutable snapshot identity through the owning session service.
Fix:
Resolve snapshot, document, validation, and diagnostic requests through one fixed-source path; require query, header, and snapshot identities to agree; keep storage authority server-local; and share cold diagnostic serialization across local and remote exports. Runtime metrics are included only while the same controller remains published for the source.
Verification:
Serve and Desktop regressions cover foreground switches, takeover, identity conflicts, deletion, snapshot boundaries, cold diagnostics, target preservation, and frozen remote routing. Focused race tests and the real Chromium PDF/image export flow pass.
Problem:
Late cancellation of a completed historical preparation could target a newer retry for the same source.
Root cause:
Preparation operation IDs were derived from the stable source ID, so distinct task instances shared one cancellable identity.
Fix:
Assign a fresh opaque operation ID to every new task, retain same-source joining only while the task is active, remove superseded terminal records, and fence stale completion publication. Deduplicate frontend cancellation by captured operation ID.
Verification:
Focused lifecycle regressions cover concurrent joining, late cancellation, cancellation retry, and interactive/batch demand isolation. The frontend banner regression and typecheck pass.
Problem
desktop-windows-go-group (A-B) failed
TestAIRenameCanonicalSessionUsesDurableHistoryInsteadOfEmptyLegacyFile/session-id
with AIRenameSession = "", session_operation:operation_failed. The subtest
ran 51.5s against roughly 30s for its siblings, and the generic product
text carried no cause.
Root cause
aiRenameCanonicalSession wrapped the whole operation in its own 30s
deadline, duplicating a budget the provider call already owns:
control.generateSessionTitle passes sessionTitleTimeout (30s) to
boundedllm.Call, which bounds the model round trip from the call. The
host clock therefore also bounded the durable snapshot, the runtime
flush, the history projection TitleMessages builds, two process mutexes
that do not observe a context, and the conditional commit. That
preparation costs 2.4s for this fixture's 113-event conversation on an
idle macOS host and 4.6s under load, and several times that on a Windows
runner, so a slow host spends the model's budget on local I/O and the
already generated title is refused by a deadline that belongs to the
provider. context.DeadlineExceeded matches no classifier, so
sessionOperationErrorForTarget collapsed it to operation_failed.
aiRenameLegacySession never had this second clock.
Fix
Drop the duplicated host deadline. The canonical rename now runs on the
operation's own cancellation, exactly like the legacy twin and like the
manual rename, whose commit already goes through
WithSessionUnchanged(a.bootContext(), ...). The model round trip stays
bounded at 30s where it is made. sessionOperationErrorForTarget also logs
the unclassified cause, so the next such failure is diagnosable from a CI
transcript instead of collapsing to the generic text.
Verification
New TestAISessionTitleBudgetBelongsToTheModelRoundTrip asserts the model
is handed its full budget; it fails before this change ("durable
preparation of 1.33s left the model only 28.67s of its 30s budget") and
passes after. go test ./ -count=1 (full desktop suite, 390s), the failing
CI partition go test ./ -skip '^Test[C-Z]', and go test -race on the AI
title tests all pass. gofmt, go vet, golangci-lint on desktop are clean;
go run ./tools/repolint reports clean (1185 baselined findings). The root
module is untouched.
Co-authored-by: SivanCola <[email protected]>
Problem: native upgrade evidence showed readable history with a stale Import and open action.
Root cause: canonical bind and resume paths assigned identity fields directly; frontend seed merges also retained omitted historical-source metadata.
Fix: route canonical binding through the identity setter, explicitly clear absent preparation metadata during seeding, and require native acceptance to reject leftover preparation controls.
Verification: reproduced failures in canonical binding, open, and both compatibility resume paths before the fix. Focused race tests, frontend banner and tab metadata tests, test typecheck, production build/budget, lint and inventory checks now pass. Startup payload remains within the existing adjusted budget.
Problem: Windows could not remove the upgrade fixture directory after all functional assertions passed.
Root cause: production saved-tab reconciliation opens the draft SQLite store, but the fixture only closed session services.
Fix: drain preparation work and close session services plus the draft database exactly once per simulated launch, including early test failure cleanup.
Verification: focused upgrade-fixture race test passes locally. The unchanged production package passed native Windows installation, explicit historical preparation, visible transcript, data preservation, restart and normal exit on the preceding commit.
Problem: packaged upgrade startup treated retained historical tabs as corrupt and expected automatic conversion.
Root cause: saved-tab reconciliation had no unprepared-source state, and upgrade acceptance still asserted the retired eager migration flow.
Fix: project discovered sources into neutral pending tabs, route the explicit action through normal navigation, and omit empty probe directories. Update native acceptance to click only on first import while preserving transcript, restart, and data-integrity assertions. Record the measured 243-byte startup metadata increment with a bounded raw-size allowance.
Verification: focused startup/reconciliation tests; historical and saved-tab race tests; TypeScript, navigation, banner, recovery and controller-meta tests; frontend production build and bundle checks; golangci-lint, repository lint and generated inventory. Native Windows acceptance remains for CI.
Problem
test (windows-latest) failed: TestCanonicalTakeoverCommandReportsRefusedGrant
could not remove its TempDir because sessions-v4/.held.ownership.lock was
still open. Linux and macOS allow unlinking an open file, so the same code
passed there.
Root cause
Two tests open a second session.Service to really hold a session's writer, and
discard the ClientBinding that Open returns. Their cleanup called CloseAll,
which Service documents as the runtime-only primitive: it does not close a
runtime whose client never unbound, so the ownership lock survived the test.
Service.Shutdown is the documented lifecycle boundary a short-lived service
must use before its root directory is removed, and it is terminal.
Fix
Shut both holder services down instead of closing their runtimes, so the
writer lease and recovery handles are released before TempDir removal.
Verification
go test ./internal/cli/ and go test -race ./internal/cli/ pass; gofmt and
repolint clean. The Windows-only unlink behaviour cannot be reproduced on
macOS, so CI is the confirming run.
Co-authored-by: SivanCola <[email protected]>
Problem
The desktop-prepare CI job failed its bundle budget: the Linux stable build
measured 2073980 B (2025.4 KiB) against the 2025.3 KiB ceiling set from a
macOS build, 73 B over.
Root cause
The ceiling was ratcheted from the same-toolchain macOS build (2073794 B).
The CI producer builds 186 B larger, and 0.11 KiB of headroom did not cover
that platform delta.
Fix
Set the ceiling from the CI measurement, 2025.5 KiB, and record both numbers
so the next ratchet accounts for the platform delta. Other budgets are
unchanged.
Verification
pnpm check:bundle locally: 2025.2 KiB / 2025.5 KiB passes; CI value 2025.4 KiB
also fits.
Co-authored-by: SivanCola <[email protected]>
Problem: Source-only pinned shells could mutate the workspace registry during startup listing, and initial metadata discovery was not tracked by shutdown.
Root cause: Shell projection reused organization initialization, while startup discovery bypassed coordinator worker ownership.
Fix: Project unprepared pins from read-only catalogs without creating workspaces. Track startup discovery with the coordinator context and worker lifetime so shutdown cancels and drains it before closing services.
Verification: Historical, startup, source-only pin, and catalog revision regressions pass, including deterministic shutdown ordering and focused race coverage. Go lint, repository lint, generated inventory, and diff checks pass.
Problem
go test -race ./... reported a data race in internal/cli: a fixture's
background takeover loop read the serve-discovery seam while the next test's
fixture swapped it, failing TestResumeCanonicalSessionHeldElsewhereKeepsMirror.
Root cause
newCanonicalTakeoverTUI builds a cliTakeoverManager but never shuts it down.
Activate starts the manager's run loop, which calls readoptLocked and reads
the package-level discoverCLIServesForTakeover. That loop outlived its test,
so it was still reading when withFakeCanonicalDiscovery installed or restored
the fake for another test. Every test that activates a manager shares this
fixture, so the leak was one per activating test.
Fix
Register the manager's Close with the fixture that creates it, after the
controller and service cleanups so it runs before them. Close already closes
the stop channel and waits for the loop to exit, so the goroutine is joined
rather than merely signalled, and the seam can no longer be written while a
reader is live. Production code is unchanged.
Verification
go test -race -count=1 ./internal/cli/ three consecutive times: no races, no
failures. gofmt clean.
Co-authored-by: SivanCola <[email protected]>
Problem: Removing the only remote surface passed alone but failed in the full Desktop suite with a workspace mutation conflict.
Root cause: Remote fixtures replaced the config home while retaining the package-wide canonical registry. Its global workspace still referred to an earlier config home.
Fix: Reuse the complete Desktop directory-isolation helper for remote host fixtures. Add a deterministic regression that seeds an existing global workspace before creating the next fixture.
Verification: The new regression failed before the fixture repair. Remote and host-removal tests and focused race checks pass after repair; Go lint, repository lint, and inventory freshness checks pass.
Problem: Unprepared canonical sources and saved pins were missing from normal navigation, shutdown could discard a batch, concurrent sidecar writers could erase each other, and late cancellation could replace a newer page.
Root cause: Discovery was isolated from ordinary catalog projections, queue lifetime was not drained with conversion workers, writes replaced cached whole-state snapshots, and recovery and UI responses depended on mutable source or navigation state.
Fix: Publish historical metadata into scoped lists and route sidebar opens through preparation. Preserve durable batch selections with worker ownership and revisioned read-modify-write updates. Recover published targets independently of retained sources and fence asynchronous UI responses by identity, revision, and navigation intent. Preserve unknown sidecar fields and document rollback constraints.
Verification: Deterministic historical and sidecar regressions, focused Desktop race tests, Go lint and repository lint, inventory freshness, TypeScript checks, all 417 discovered frontend suites, production frontend build and unchanged bundle budgets passed. Browser smoke checks covered the real app layout with mock data. Desktop full-module and exact-head CI checks are tracked separately.
Problem
After merging main-v2, TestOpenSessionUsesDormantLocalTabWithoutAnActiveTab
and TestActiveOrSingleLocalTabRecoversLeaseBlockedStartup failed: opening a
canonical session with no active tab built a second surface next to the tab a
remote-only layout had restored for local work, leaving that tab unbound.
Root cause
6cdb5fedf replaced this branch's activeOrSingleLocalTab call in
openSessionWithNavigation with surfaceForCanonicalSession, which resolves the
active tab, then a tab already running the session, and otherwise creates a
surface. It has no step for the dormant tab this branch restores when the
visible surface is remote, so that tab could never receive an open.
Fix
Give surfaceForCanonicalSession that missing step: after the running-session
match and before creating a surface, reuse the first visible tab that resolves
through tabAndCtrlByID, activating and persisting it like the other reuse
paths. activeOrSingleLocalTab now shares that resolver, so both entry points
agree on which tab local work targets and both retry a startup that a session
lease blocked. Reuse is deliberately not restricted to tabs without a session:
the active-tab path already installs a session into whichever tab is current,
and restricting only this path would have changed behavior the branch shipped.
Verification
go test ./ -run 'TestOpenSessionUsesDormantLocalTabWithoutAnActiveTab|
TestActiveOrSingleLocalTabRecoversLeaseBlockedStartup|
TestOpenSessionWithoutSurfaceCreatesOneForTheSession|
TestOpenSessionActivatesVisibleTabAlreadyRunningTheSession|
TestDiscardUnboundSurfaceRemovesOnlyTheEmptyTab|
TestRemoteOnlyRestoreBuildsOwnedDormantTabWithoutTakingTheSurface|
TestOpenSessionAdoptsRuntimePublishedWhileWaitingForTheRebuildLock' passes;
gofmt, go vet, golangci-lint and repolint clean.
Co-authored-by: SivanCola <[email protected]>
Conflict resolutions:
- desktop/session_workspace_api.go: keep main-v2. 6cdb5fedf replaced the
active-tab lookup in openSessionWithNavigation with
surfaceForCanonicalSession, which creates a surface when none exists.
Its first step is still tabAndCtrlByID(""), so the startup-lease
recovery this branch restored on that path is preserved.
- desktop/frontend/scripts/check-bundle-budget.mjs: keep this branch's
ledger entry and re-measure against the advanced base. The merged
Electron build is 2073794 B (2025.2 KiB), 7754 B over main-v2's
2066040 B; the ceiling moves to 2025.3 KiB with 0.11 KiB headroom.
Co-authored-by: SivanCola <[email protected]>
Problem:
Opening a remote project tab with New Session stopped publishing a ready
remote-tab:updated as that tab's first meta: the bootstrap emitted a
"connecting" meta first, so the sidebar's new project group re-pulled the
session listing while the fresh session did not exist yet.
TestBootstrapNewSessionPublishesTabUpdateForSidebarBlankRow failed with
`bootstrap update state = ready:false remote:"connecting"`.
Root cause:
installRemoteTabAttachPump announced every attach for history-first
hydration, including a fresh-session attach, which installs no route
(installRoute is !opts.NewSession) and therefore publishes a meta with no
session identity at all. That meta can prime no persisted window, and it
front-runs the ready meta that bootstrapRemoteTab publishes for a fresh
session, which is the sidebar's only signal to re-pull the blank row. The
announcement also emitted after the route fence was released, so it could
interleave with another publication for the same tab.
Fix:
Publish the attach identity only when the attach committed a session route,
and publish it inside the routeEventMu fence that installed that route, so
the announcement keeps the file's single publication order. A fresh
bootstrap now stays silent until its own ready meta; routed attaches (cold
open of a saved session, revived shell, serve restart) still announce while
connecting, so history-first hydration is unchanged. The new
TestRoutedAttachAnnouncesIdentityWhileConnecting pins that half so the
announcement cannot be dropped to satisfy the sidebar contract.
Verification:
- cd desktop && go test ./ -count=1 (full suite, ok 367.9s)
- cd desktop && go test ./ -count=1 -race -run
'RemoteTab|Reclaim|Takeover|Runtime|TabSelection|RemoteListing|
RemoteProject|RemoteTranscript|Bootstrap|Persist|Mirror' (ok 71.4s)
- gofmt -l desktop/ clean; cd desktop && go vet ./ and golangci-lint run ./
clean; go run ./tools/repolint clean (1213 baselined findings)
- Event order for a New Session bootstrap is now state:connecting,
remote-tab:opened, session_start/ready frames, state:ready,
remote-tab:updated(ready, sessionPath set); a routed open keeps its
connecting remote-tab:updated carrying the session path before ready.
Co-authored-by: SivanCola <[email protected]>
The generated host contract records the source file of every exported App
method, so moving ReclaimRemoteTabSession to remote_tab_reclaim.go made
TestHostCommandOwnersMatchSource and TestHostContractGeneratedFilesAreCurrent
fail on a stale digest.
Return ReclaimRemoteTabSession to remote_tab.go and take the unexported
spectator-pin trio (markRemoteTabSpectatorIfLocalOwned,
probeSpectatorAfterNotice, clearRemoteTabSpectator) out to
remote_tab_spectator.go instead, which keeps remote_tab.go under its size
budget without touching the contract. Compress the two comments that moved
with them.
Verified with `go run . -emit-contract` into a scratch directory: both
generated files are byte-identical to the committed ones.
Co-authored-by: SivanCola <[email protected]>
ProjectTree.tsx and the remote surface suite were each over their recorded
size by a few lines.
Move the "can this remote row be trashed" predicate next to the identity
helper it calls, as remoteSessionArchiveBlocked in ProjectTreeRemoteGroups,
and call it from the trash menu item. Same expression, same operand order.
Move the remote surface suite's jsdom bootstrap into
helpers/remoteSurfaceDom.ts. The globals are installed at the same point in
module evaluation as before, so import order and every stubbed measurement
are unchanged.
Co-authored-by: SivanCola <[email protected]>
Three test files were over the 800-line ceiling because each carried more than
one scenario. Split them along the scenario they cover, with no test text
changed:
- remote_tab_session_transition_test.go takes the /resume and /new transition
outcomes (busy, rejected, lease conflict, list failure, frame buffering).
- remote_serve_fake_test.go takes the shared fakeServe harness and its
helpers, which several suites already use.
- session_reclaim_test.go takes the ownership-probe and reclaim-barrier cases,
leaving the takeover grant and mirror cases behind.
Each moved function is byte-identical to its original; only the import lists
differ, per file. Verified by comparing the sorted non-import body lines of
each original against its two successors.
Co-authored-by: SivanCola <[email protected]>
session_takeover.go held both the takeover command and the whole mirror
transport loop. Move run/pushOnce/retryAdoptOrDemote/drainQueue/flushOnce and
their locked halves into session_takeover_mirror_loop.go, leaving the command
and mirror registry behind.
Break recordRemoteTabSessionStatus into named steps —
remoteTabStatusRouteRejectedLocked, resolveRemoteTabOwnershipBarrierLocked and
remoteTabStatusMetaChanged — and lift the resume pre-check out of
resumeRemoteTabSessionPathForOpenSelection into admitRemoteTabResume. The two
long explanations become doc comments on the steps they describe.
Move terminalReadOnlyForTab next to the read-only policy it belongs to, and
compress three inline comments to the invariant they carry.
Behaviour-preserving: identical conditions, lock order and return values; the
route/ownership guards keep their original evaluation order.
Co-authored-by: SivanCola <[email protected]>
remoteTab carried 14 independent scalars, two of which — reclaimRevision and
pendingReadyBarrier — share one lifetime: a committing reclaim sets both and
the re-hydrated surface clears both. Group them into remoteTabOwnershipState
so the pair can only be read as one state, dropping the struct to 12 scalars.
Move the reclaim command (remoteTabReclaimObservation,
observeRemoteTabForReclaim, ReclaimRemoteTabSession and the
remoteSessionTakenOver refusal predicate) out of remote_tab.go and next to
the ownership probe that already lived in remote_tab_reclaim.go, and compress
three over-long inline comments to the invariant they carry.
Pure relocation and renaming: no call, lock order, fence comparison or emitted
event changed.
Co-authored-by: SivanCola <[email protected]>
Problem: New draft sessions could keep a stale model, expose canonical session routes as titles, and diverge from their frozen retry model.
Root cause: Draft storage did not distinguish inherited defaults from explicit model choices, async catalog projections were not version-fenced, and runtime labels treated canonical routes as filenames.
Fix: Track model provenance, resolve inherited defaults live until submission, freeze canonical execution settings, fence frontend refreshes, preserve retry identity, and keep internal session routes out of display labels.
Verification: Go focused and race tests, go vet, repolint, generated contract refresh, frontend lifecycle tests, typecheck, lint, and production build.
chat_tui.go was over all four of its repolint budgets after this PR
series added reclaim gating to update() and runSlashCommand().
- The three reclaim flags become one embedded reclaimState in
chat_tui_reclaim.go. It is embedded, so m.sessionReclaimed and friends
still resolve through field promotion and no caller or test changes.
The flags share a lifetime, which is why they now share a struct.
- The composer and slash-command gates become reclaimBlocksInput and
slashInputBlockedNotice next to sessionDetached, the predicate they
already duplicated. reclaimInputAllowed on a bare typed command is the
same four-command test the slash gate spelled out inline.
- runSlashCommand's default branch (docs page, custom command, skill,
extension action, prose fallback) moves verbatim to
runUnrecognizedSlash in chat_tui_slash_fallback.go. It always returned,
so the switch arm is now a single return.
No condition changed its operands or its evaluation order. chat_tui.go
4945 -> 4881 lines, runSlashCommand 265 -> 214 lines / complexity 82 ->
62, update 997 -> 993 / 255 -> 252.
Co-authored-by: SivanCola <[email protected]>
runAgent and chatREPL each inlined the same two steps this PR series
grew: turning --resume/--continue/--copy into a cliResumeTarget, and
committing that target with a canonical takeover when the writer is held
elsewhere. Both functions were past their repolint size and complexity
budgets, and cli.go was past its file ceiling.
cli_resume_start.go now owns headlessResumeTarget, interactiveResumeTarget,
normalizedResumeFlag, continueResumeTarget, copySessionExitCode and
commitStartupResume, plus the two takeover-approval policies (--takeover
answers yes up front, the TUI asks). The callers keep the exit codes they
returned inline.
The predicate chain in commitStartupResume is the original condition with
De Morgan applied, so canonical(), errors.Is and the approval callback are
still evaluated in that order and the prompt still runs only for a held
final-format session. Counts: cli.go 2799 -> 2724 lines, runAgent 326 ->
293 lines / complexity 63 -> 51, chatREPL 418 -> 377 / 76 -> 60.
Co-authored-by: SivanCola <[email protected]>
session_takeover_cli.go had grown to 1137 lines, 142 weighted units past
its repolint budget, by stacking two concerns: finding the resident serve
that holds a session, and the mirror manager that owns the session once
the serve lets go.
The discovery half moves verbatim to session_takeover_serve.go: the
handoff wire types, discoverCLIServes and its PID probe, cliServeClient,
cliTakeoverHeldSession, postCLITakeoverHandoff, cliTakeoverFromServe, the
takeover-candidate predicate and the terminal prompt. The manager, its
binding and the pending-return machinery stay behind (873 lines). The one
orphaned doc line naming cliTakeoverFromServe is reattached to it.
Also compresses three over-long comments to three lines each: the file
header, cliTakeoverBinding.canonical and cliTakeoverManager.yielded.
Declarations move between files in the same package and keep their
bodies, so no call resolves differently.
Co-authored-by: SivanCola <[email protected]>
The takeover review series pushed four CLI files past their repolint
budgets with commentary rather than logic.
- session_takeover_canonical_test.go crossed the 800-line ceiling; the
mirror-return cases (resumeEntryIndex, assertMirrorLeft and the five
Resume*Mirror tests) move verbatim to session_takeover_mirror_test.go.
Same package, same assertions, no case added or dropped.
- resume.go, resume_test.go, chat_tui_reclaim.go and chat_tui_shutdown.go
had 4-6 line block comments where the rule allows 3. Each is rewritten
to state the same invariant in three lines; none is deleted.
Comment text and test placement only, so behavior is unchanged.
Co-authored-by: SivanCola <[email protected]>
session_ownership.go carried the whole single-writer takeover protocol
in one 1749-line file, far past its size budget. Move whole declarations
into files named after the concern they own:
- session_handoff.go: the /handoff request, the quiet-then-release loop,
and both the identity and path release transactions.
- session_reclaim.go: /reclaim, the identity and mirrored reclaim
transactions, the lease rollback, and the stale-mirror auto-reclaim.
- session_ownership.go keeps ownership resolution, the mirror registry
and the read-only views a spectator sees.
The 15-line protocol essay that opened session_ownership.go is package
explanation, so it moves to the new doc.go together with the package
comment that used to sit on serve.go; each file now points at it in
three lines. Remaining over-long blocks are compressed to the invariant
they state.
Pure moves: no declaration changed body, order, lock scope or receiver,
so behavior is unchanged.
Co-authored-by: SivanCola <[email protected]>
serve.go and sessions_http.go both carried PR-added code past the
repository size, function-size and complexity ceilings. Move whole
declarations into files named after the concern, and extract helpers
that state the step they perform:
- status_http.go takes Server.status and splits it into the identity
route, the spectator view, the foreground snapshot, the effort block,
the mirrored override and the optional balance fetch.
- session_delete.go takes deleteSession and its locked helpers.
- serve.go keeps switchModelLocked but hands the profile system-message
splice and the session-axis inheritance to named functions.
- sessions_http.go splits Server.sessions into the legacy rows, the
canonical catalog walk and the merge, with the migration fold stated
once on mergeSessionRows.
- control/session_reclaim_events_test.go takes the two reclaim/handoff
tests that pushed session_events_test.go over the file ceiling.
Over-long comment blocks are compressed to the invariant they carry.
No behavior change: every moved statement keeps its order, lock scope
and early returns, the canonical preview fallback keeps its own clamp
rather than adopting previewTitle's paste-label stripping, and the
runtime-only query read is the same predicate after De Morgan.
Co-authored-by: SivanCola <[email protected]>
Problem
The review-fix series pushed three files further past their recorded
repolint debt: comment blocks over the 3-line limit for their position,
and closeTabRuntime grew past the function-size ceiling.
Root cause
Several new comments restated the commit message rather than the
invariant, and the mirror farewell claim was written inline in
closeTabRuntime instead of behind the boundary that owns it.
Fix
Trim the new comments to the invariant they protect and move the claim
and its release into claimTakeoverMirrorFarewell. The release is now
unconditional: it runs once the close has finished with the writer, so
the loop is only ever unmuzzled when announcing the writer is safe.
Verification
cd desktop && go test ./ -run 'ClosingTabOwns|UndeliveredMirror|AbandonedClose|IdentityRouted|OwnershipProbe|Reclaim|ResumeAcceptsCommitted|SyntheticCurrentRow|CloseTab|Takeover' -count=1
cd desktop && go vet ./ && golangci-lint run ./
repolint: the essay and complexity overages this series added are gone;
closeTabRuntime is back under its ceiling.
Co-authored-by: SivanCola <[email protected]>
Problem
Serve stopped dropping takeover and reclaim notices on identity routes,
so those notices now reach the remote tab. One landing around an
explicit take-back could re-pin the spectator banner and re-lock the
composer immediately after ownership returned.
Root cause
A takeover or reclaim notice drives an ownership probe whose answer is
applied later. That apply path fenced the tab, generation, client,
selection and route, but not the reclaim epoch, so an answer describing
the ownership a reclaim had just ended was still treated as current.
Status payloads already carry that fence; the probe bypassed it.
Fix
Capture the reclaim epoch with the rest of the probe's fences and drop a
locally-owned answer when a reclaim completed while the probe was in
flight. Clearing the pin stays unfenced: it agrees with the reclaim.
Verification
cd desktop && go test ./ -run 'Takeover|Reclaim|Spectator|IdentityRouted|RemoteTab' -count=1
cd desktop && go vet ./ && golangci-lint run ./
New: TestOwnershipProbeCannotRepinAcrossACompletedReclaim fails without
the fence; TestIdentityRoutedTakeoverNoticeProbesAndPinsSpectator,
TestIdentityRoutedReclaimedNoticeClearsSpectator and
TestIdentityRoutedNoticeForAnotherSessionIsIgnored pin that identity
routed notices probe and route coherently now that they arrive.
Co-authored-by: SivanCola <[email protected]>
Problem
Closing a tab that mirrored a session to a resident Serve announced the
writer as gone while that writer was still being released, so Serve had
to wait for the writer to drop before handing the session back.
Root cause
The farewell was driven by the mirror loop's tab-gone poll, which fires
as soon as the tab binding disappears from the registry. The close then
still had to quiesce autosave, close the controller and release the
lease — all after the announcement. Shutdown already had the correct
epilogue (endTakeoverMirrors); a single tab close had no equivalent.
Fix
The closing tab claims the farewell while it still owns the writer, and
sends it from the close epilogue once the lease is released. The loop's
tab-gone branch keeps detaching and stays the backstop for a tab that
disappears without that epilogue, and a close that returns early —
a detached runtime, or a tab that changed underneath — hands the claim
back. A delivered farewell is recorded so the two paths cannot both
announce it; an undelivered one stays retryable.
Verification
cd desktop && go test ./ -run 'Takeover|Mirror|CloseTab|Close' -count=1
cd desktop && go vet ./ && golangci-lint run ./
New: TestClosingTabOwnsTheMirrorFarewellUntilTheWriterIsReleased,
TestUndeliveredMirrorFarewellStaysRetryable and
TestAbandonedCloseReturnsTheFarewellClaim.
Co-authored-by: SivanCola <[email protected]>
Problem
Opening a session on the workspace tab a remote-only layout restores
could still fail with "tab runtime changed while opening session" when a
concurrent activation built that tab's runtime, even though the open had
resolved the tab precisely because it had none.
Root cause
The nil-controller adoption read the tab's runtime at the top of
resumeCanonicalSessionForTranscript, before runtimeRebuildMu was held. A
runtime published during that wait made the later authoritative read
differ from the adopted value, so the equality fence rejected the open.
Fix
Adopt the runtime at the authoritative read inside the rebuild lock and
validate its identity protocol there, through a shared helper that
treats a nil runtime as a dormant tab rather than a protocol failure. A
build that publishes after this point is still rejected, by the
publication fences that own that boundary.
Verification
cd desktop && go test ./ -run 'Dormant|Restored|OpenSession|Resume|RemoteOnly|SingleSurface' -count=1
cd desktop && go vet ./ && golangci-lint run ./
New: TestOpenSessionAdoptsRuntimePublishedWhileWaitingForTheRebuildLock
fails on the PR head, and
TestRemoteOnlyRestoreBuildsOwnedDormantTabWithoutTakingTheSurface pins
the restored layout: a dormant tab with a resolvable workspace identity,
the remote shell keeping the visible surface, and a runtime built only
on first activation.
Co-authored-by: SivanCola <[email protected]>
Problem
Opening a session on a tab whose startup had been refused by a held
session lease failed with a runtime error, even after the lease holder
exited. Before this PR the same click recovered the tab and opened the
session.
Root cause
OpenSession moved from tabAndCtrlByID("") to activeOrSingleLocalTab,
which read tab.Ctrl directly. tabAndCtrlByID is the resolver that
retries a startup blocked by a lease (tryRecoverStartupLeaseHeldTab);
reading the field skipped that recovery and reported the nil runtime.
Fix
Resolve both the active tab and the dormant fallback through
tabAndCtrlByID, so every local-command target gets the same recovery
attempt before a missing runtime is reported. The controller is still
returned with its tab, so a rebuild in between cannot look like a
replaced runtime.
Verification
cd desktop && go test ./ -run 'TabSelection|OpenSession|Dormant|LeaseBlocked|ActiveOrSingle|Restored' -count=1
cd desktop && go vet ./ && golangci-lint run ./
New: TestActiveOrSingleLocalTabRecoversLeaseBlockedStartup fails on the
PR head.
Co-authored-by: SivanCola <[email protected]>
Problem
Pressing take-back could panic inside a Wails binding — a nil pointer
dereference that takes the window down — when the remote tab closed in
the moment between resolving the command target and snapshotting the
tab, because the reclaim then locked that nil tab after its 20s poll.
Root cause
The reclaim read a.remoteTabs[tabID] into observedTab without checking
the result, treating the earlier remoteTabCommandTarget call as proof
the entry still existed. Those are two separate lock acquisitions.
Fix
Snapshot the reclaim's fences through observeRemoteTabForReclaim, which
owns that lifetime boundary: a missing entry or a client replaced by a
reconnect is reported as a disconnected tab instead of being carried
forward as a nil or retired binding.
Verification
cd desktop && go test ./ -run 'Reclaim|Takeover|RemoteTab' -count=1
cd desktop && go vet ./ && golangci-lint run ./
New: TestReclaimObservationRejectsMissingOrReplacedBinding and
TestReclaimRemoteTabSessionRejectsClosedTab.
Co-authored-by: SivanCola <[email protected]>
Problem
Collapsing the persisted layout while a remote surface was active
dropped unknown top-level keys from desktop-tabs.json, so state written
by a newer build did not survive a run of this one.
Root cause
The new local-kept branch of singleSurfaceTabsFile builds its result
literal without the extra field every sibling branch clones.
Fix
Clone the forward-compatibility fields in that branch too.
Verification
cd desktop && go test ./ -run 'Restore|Persist|SingleSurface|RemoteTabRestore' -count=1
cd desktop && go vet ./ && golangci-lint run ./
New: TestSingleSurfaceLocalKeptBranchPreservesUnknownKeys fails on the
PR head and also pins that the clone does not alias the caller's map.
Co-authored-by: SivanCola <[email protected]>
Problem
After a restart an untitled legacy remote session showed the workspace
name in the tab while the sidebar showed the transcript basename, so the
same conversation carried two different names.
Root cause
The restore path cleared the persisted title whenever it equalled the
session name. For a legacy row the name is the transcript basename,
which is the legitimate user-facing label the sidebar also renders; only
a canonical session ID is the opaque token the cleanup targets.
Fix
Apply the cleanup only when a canonical SessionID is present. A legacy
row keeps its persisted basename title; a canonical row still falls back
to the localized workspace name.
Verification
cd desktop && go test ./ -run 'Restore|Persist|SingleSurface|RemoteTabRestore' -count=1
cd desktop && go vet ./ && golangci-lint run ./
New: TestRestoreKeepsLegacyBasenameTitleAndDropsOpaqueIdentity fails on
the PR head.
Co-authored-by: SivanCola <[email protected]>
Problem
A background remote session whose last observation was "executing" kept
being reported as synced after the tunnel dropped, so the project tree
showed a frozen snapshot as live runtime state.
Root cause
Dropping takenOver from the background freshness rule also dropped the
connection and sync-failure checks. runtimeStates are seeded once and
never cleared on suspend or park, so without those checks a retired
stream still presents its last snapshot as current.
Fix
Keep the disconnected and sync-failed conditions for background routes
and remove only the takenOver term, which is the one fact about the
foreground that says nothing about another session.
Verification
cd desktop && go test ./ -run 'Runtime|Freshness|RuntimeState' -count=1
cd desktop && go vet ./ && golangci-lint run ./
New: TestBackgroundRouteFreshnessFollowsTheTabConnection fails on the PR
head and pins that a taken-over foreground keeps background routes
synced.
Co-authored-by: SivanCola <[email protected]>
Problem
Pinning a remote session that had not been chatted in yet removed it
from the sidebar: the row the user explicitly asked to keep disappeared
on the next listing refresh.
Root cause
The new blank-row filter dropped zero-turn canonical rows before the
loop computed Pinned, so the pin preference could not influence the
decision that hid the row.
Fix
Resolve the pin preference before the filter and keep any pinned row.
An unpinned never-chatted session still disappears, matching the local
blank-tab contract.
Verification
cd desktop && go test ./ -run 'Pinned|RemoteProjectSessions|RemoteListing' -count=1
cd desktop && go vet ./ && golangci-lint run ./
New: TestPinnedZeroTurnSessionSurvivesTheBlankRowFilter fails on the PR
head.
Co-authored-by: SivanCola <[email protected]>
Problem
Clicking the current blank row of a migrated project raised an error
banner. The row's resume request reached Serve as
{"path":"session-id:X"}, which Serve resolved as a filesystem path and
rejected with 400.
Root cause
After the identity-first route flip, tab.routing.currentPath holds
"session-id:X" for canonical sessions, but the synthetic current row
copied that route straight into RemoteSessionView.Path and left
SessionID empty. The ID-only resume path that could have repaired it
also required a non-empty name, so the nameless synthetic row fell
through to a listing lookup keyed on "" and reported "not found".
Fix
Invert the route when building the synthetic row: an identity route
becomes SessionID, a legacy path stays Path. Accept a committed session
identity independently of the display name, and label an ID-only target
with the workspace name until the title refresh lands. The fake Serve's
/resume now mirrors the real handler (sessionId is sufficient, and the
identity header comes back), so identity routes are exercised at all.
Verification
cd desktop && go test ./ -run 'TestSyntheticCurrentRow|TestResumeAcceptsCommittedIdentity|RemoteListing|RemoteProjectSessions|RemoteSession' -count=1
cd desktop && go vet ./ && golangci-lint run ./
New: TestSyntheticCurrentRowCarriesIdentityAsSessionID and
TestResumeAcceptsCommittedIdentityWithoutAName fail on the PR head;
TestSyntheticCurrentRowKeepsLegacyPathRoute pins the legacy shape.
Co-authored-by: SivanCola <[email protected]>
Problem
Re-clicking the session a remote tab already shows put the tab into the
"switching sessions" state, and a /resume whose response the tunnel lost
left that state behind for good: after the reattach every command,
including RemoteTabStatus and RemoteTabSnapshot, was refused and live
frames for the route buffered forever.
Root cause
commitRemoteTabOpenRegistration opened routing.rehydratingPath for every
committed route, including the confirmed current one, but only the
different-route provisional-resume transaction owned the field. Its
success drain and rollback were the only writers that cleared it, and
the transport-failure branch that hands the tab to reconnect cleared
nothing. More generally, any pump generation retired mid-resume (dead
stream, host suspend, Serve park) left the gate open, because every
commit and rollback path fences on that generation.
Fix
Open the gate only when a ready tab commits a route Serve has not
confirmed (registration and the deferred-selection commit alike); a
same-route re-selection never enters it. Close the epoch whenever a pump
generation is retired (retire, reconnect, suspend, park), since a retired
generation can no longer drain or roll back and the reattach reconciles
the committed route against Serve's foreground.
Verification
cd desktop && go test ./ -run 'Resume|Switch|PendingSelection|Reattach|Reconnect|Provisional|Reselect|RemoteTabCommand' -count=1
cd desktop && go vet ./ && golangci-lint run ./
New: TestRetiringPumpGenerationClosesProvisionalRouteEpoch,
TestRemoteResumeTransportFailureReattachAcceptsCommands and
TestReselectingCurrentRemoteSessionDoesNotEnterSwitchingState all fail
without this change.
Co-authored-by: SivanCola <[email protected]>
Problem
A reattach only re-entered a session when the Serve instance had
changed. A first-open tab (no instanceID yet) or a tab whose Serve
foreground another client had moved published ready on whatever Serve
was running; the next /status adopted that foreground and the tab
silently showed a different session than the one the user clicked (or
the New Topic they asked for). Before the PR routed refused /events
opens into the reattach loop this surfaced as an error with retry.
Root cause
reattachRemoteTabOnce treated "same Serve instance" as "Serve still
holds this tab's session" and never compared the foreground against the
tab's committed selection.
Fix
Snapshot the tab's selection (route, name, identity, New Topic intent)
before the network work. On a surviving Serve, probe the foreground and
re-enter the selection through /new or /resume when it differs; a
replacement Serve always re-enters. Busy and taken-over refusals follow
the first-attach contract (follow the current session / spectator pin),
and a recovered New Topic keeps bootstrap's reusable-blank contract.
Verification
cd desktop && go test ./ -run 'Reattach|Reconnect|Relaunch|Replacement|ServeDown|FollowsHost|TitleRefreshKeepsBlank|DefersCached|PumpConnectionFailure|RevivesServeDown|ReviveApplies|FocusOnly|SavedSessionLookup|HostStatus|Suspend' -count=1
cd desktop && go vet ./ && golangci-lint run ./
New: TestRemoteTabReattachReentersSelectedSessionWhenServeMoved and
TestRemoteTabFirstOpenReattachEntersRequestedNewSession fail on the PR
head; TestRemoteTabReattachKeepsStreamWhenServeStillRunsSelection pins
the no-transition path.
Co-authored-by: SivanCola <[email protected]>
Problem:
The Global folder and one project both failed to load in the sidebar with
"workspace path matches multiple persisted workspaces: global, project-<hash>",
and nothing in the app could recover them.
Root cause:
A registry written before physical workspace identity could record one
directory twice, for example the Global folder's own directory also added as a
project. Path-to-owner resolution refused such a registry outright. Every
sidebar listing resolves an owner through EnsureWorkspaceResolved, so both
records stayed unreachable forever. Resolution also failed as a whole when any
single persisted root could not be resolved.
Fix:
Repair the duplicate where the registry owns the state, inside the writer
transaction. One owner is chosen deterministically (Global, else the oldest
registration, else the sorted ID) and the losing records fold into it together
with their sessions, organization and every record that addresses the
workspace by ID. ResolveWorkspaceID answers with the same owner. Records fold
only when the candidate directory exists now: a path resolved through a missing
leaf borrows an ancestor's links and case rules, so distinct directories on an
unreachable volume can share a key, and folding is not reversible. A persisted
root this host cannot resolve is skipped instead of failing every lookup. The
single-owner path still returns without rewriting persisted state, and the
registry schema is unchanged.
Verification:
cd desktop && go test ./internal/... && go test -timeout 25m .
go vet ./... ; gofmt ; go run ./tools/repolint
The end-to-end regression fails on the previous code with the reported message
and passes with the fix; a saved tab naming a folded workspace is restored.
Problem
A session lease held by another CLI on this machine was advertised with
"run /takeover to take this session over", but /takeover takes a session
from a resident serve; with no serve running the command can only fail
with "no local serve identity to take over from".
Root cause
cliSessionTakeoverCandidate returned true for any lease error carrying
holder info, without checking that a handoff endpoint exists at all.
Fix
Also require at least one resident serve on this machine. The holder's
PID still need not match a discovered serve — state-file PIDs drift
across serve restarts and desktop reconnects, and the execution already
falls back to trying every local serve — but with no serve at all the
refusal stays at naming the holder and the close hint.
Verification
New TestCLISessionTakeoverCandidateRequiresAResidentServe covers no
serve, one serve with a non-matching PID, a non-lease error, and a lease
error without holder info. Resume, takeover, and reclaim suites stay
green; gofmt/vet/golangci-lint clean.
Co-authored-by: SivanCola <[email protected]>
Problem
`--resume QUERY` matched only legacy transcripts although the CLI
advertises final-format support, so a session id, identity route, or
title that exists only in the catalog failed with "no session matches".
And `/takeover` for a canonical session the desktop had already closed
failed with "no resident serve on this machine holds this session",
contradicting the reclaim notice's promise that /takeover takes it back.
Root cause
resolveSessionQuery listed legacy sessions only. runCanonicalTakeoverCommand
treated a failed handoff as terminal, even when the failure meant nobody
holds the identity any more, so there was nothing to hand over.
Fix
Resolve queries against the same universe the picker offers
(scanWorkspaceResume: engine mirrors folded, migrated sources hidden, so
a transcript and the identity it became cannot compete as two matches):
exact session ids and session-id: routes, then title/preview substrings
across both stores, keeping the existing ambiguity and not-found errors.
For /takeover, the serve handoff stays the primary path — it is what
establishes the mirror — and only when no serve granted it does the
command consult the writer lock: an unheld identity is resumed ("session
resumed; no other runtime holds it"), while a still-held one reports why
the handoff failed. Re-taking the session this controller already writes
is rejected instead of replaying it.
Verification
New TestResolveSessionQueryMatchesCanonicalSessions (id, route, preview,
legacy preview, ambiguity across stores, not-found),
TestCanonicalTakeoverOfFreeSessionResumes (serve exited, and a resident
serve that refuses after the desktop closed the session), and
TestCanonicalTakeoverOfActiveSessionIsRejected — all three fail on the
previous code. TestCanonicalTakeoverCommandReportsRefusedGrant now holds
the writer through a second service so the refusal it asserts is the one
that matters; it passes before and after. docs/CLI.md and
docs/CLI.zh-CN.md record the /takeover fall-through. gofmt/vet/golangci-lint
clean.
Co-authored-by: SivanCola <[email protected]>
Problem
A canonical /takeover could block the TUI's update loop for roughly
2 x N x 2m15s: after every resident serve had answered (including the
wait-mode "still running; retry with mode=interrupt" verdict that already
cost a full drain window), the command re-discovered and asked every
serve again, with no spinner and no way to interrupt.
Root cause
cliTakeoverIdentityHeldSession re-ran the whole round on any error. The
second pass exists for a desktop reconnect that respawns the serve and
rewrites its state file, which only manifests as a transport failure; an
HTTP verdict was treated the same way.
Fix
Classify failures at the boundary that knows: cliServeClient and
postCLITakeoverHandoff wrap dial/request errors in
cliServeUnreachableError (transparent to %w chains). The round is
repeated once only when no serve answered; a verdict ends the command
and is the error reported. The wait-mode drain itself is unchanged and
still synchronous (noted for follow-up).
Verification
New TestCanonicalTakeoverDoesNotRetryOnServeVerdict (one handoff, one
discovery pass after a 409 "still running") and
TestCanonicalTakeoverRediscoversAfterTransportFailure (a refused dial
triggers exactly one re-discovery that then succeeds). Canonical
takeover/reclaim and manager readopt suites stay green;
gofmt/vet/golangci-lint clean.
Co-authored-by: SivanCola <[email protected]>
Problem
After the desktop reclaimed a legacy-path takeover, "/takeover takes it
back" posted session-id:<branch id> to a serve that holds a legacy path
lease, so the re-take was refused; "/takeover <n>" on a legacy row while
detached failed with "exclusive v3 controller has no bound session
runtime". A reclaim also left an open /resume picker showing stale rows.
Root cause
rememberReclaimedTarget preferred SessionRef(), which the exclusive
engine binds for legacy imports too (the transcript is mirrored under an
identity named by its branch id), so the path fallback was dead code. The
legacy branch of runTakeoverCommand always snapshotted the current
session and never cleared the reclaim state, even though a reclaimed
runtime is already released.
Fix
The mirror manager records the binding a reclaim yielded, and the TUI
routes by that binding's own kind: a legacy mirror is remembered as its
path, a canonical mirror as the identity its route names. The legacy
takeover branch skips the snapshot and lease-follow while detached,
clears the reclaim state on success, and says "resumed" rather than
"taken over" when no other runtime held the target (no mirror was
established). handleSessionReclaimed closes a stale picker overlay.
Verification
New TestLegacyMirrorReclaimRoutesTakeoverByPath (reclaim of a legacy
mirror is remembered as the path, the stale picker is dropped, and
/takeover posts no identity handoff) and
TestTakeoverOfFreeLegacySessionAfterReclaimSkipsSnapshot (detached
/takeover of a legacy row attaches and leaves reclaimed mode). Both fail
on the previous code with "reclaimed target = ref fresh-cli" and "left
the TUI in reclaimed mode". Canonical takeover/reclaim, manager, and
shutdown suites stay green; gofmt/vet/golangci-lint clean.
Co-authored-by: SivanCola <[email protected]>
Problem
TestRunResumeKeepsCompletedIndexStableAcrossRecoveryGC fails once
mergeResumeStores orders canonical rows correctly: /resume <n> lands on
recovery-parent.jsonl instead of wanted.jsonl.
Root cause
The fixture computed the numeric index before creating the controller.
Creating a controller with SessionDir and an unsaved session path leaves
an engine mirror in sessions-v4 that the catalog lists as the newest row
while its metadata is pending, so the list the command resolved against
had one more leading row than the list the index was read from. The test
passed on the PR head only because the inverted comparison parked that
newer row at the tail, and the row's visibility also raced the
asynchronous metadata rebuild (pending rows stay listed; a ready empty
row is hidden).
Fix
Create the controller first, wait for the mirror's catalog metadata to
settle, then read the index — the order a user experiences, since the
list is rendered while the controller is alive — so both listings see
the same rows deterministically.
Verification
go test ./internal/cli/ -run TestRunResumeKeepsCompletedIndexStableAcrossRecoveryGC
passes three consecutive runs; gofmt/vet/golangci-lint clean.
Co-authored-by: SivanCola <[email protected]>
Problem
Switching away from a live canonical takeover never returned the mirror:
after /resume <canonical> or /takeover <canonical B> while mirroring A,
the desktop tab for A stayed read-only and rendered the new session's
frames under A's mirror id; /resume <legacy> while mirroring A either
leaked the mirror the same way or failed with "no detached session lease
held".
Root cause
Only the legacy-to-legacy path (cliAcquireFreeSession -> priorMirror ->
commitPrevious) returned the prior mirror. commitCanonicalSessionSwitch
did OpenSession + Rebind("") and left m.binding untouched, so Emit kept
forwarding the controller's frames; runCanonicalTakeoverCommand's
Activate overwrote the binding; commitPriorMirror assumed the detached
source keeper held a legacy lease with a reverse reservation to publish,
which is false after a canonical attach (the live keeper holds nothing).
Fix
Add cliTakeoverManager.leaveMirror as the single "leave the current
binding" step every switch takes before binding the next session. It
retires the source by kind: a legacy lease publishes its reverse
reservation from whichever keeper still holds it (detached or live; a
lease nobody holds has nothing to publish), a canonical identity only
ends the mirror because its writer lock travels with the controller
binding. commitPrevious/commitPriorMirror route through it, including
when the detached keeper is nil or lease-less. commitCanonicalSessionSwitch
now follows the legacy order — secure the target with a probe grant
(service.Open, so a held target fails with ErrWriterOwned before anything
changes hands), leave the mirror, publish via OpenSession, release the
probe — which also keeps the runtime-state frame emitted while publishing
out of the old mirror's queue. runCanonicalTakeoverCommand inherits the
fix because it commits through the same function before Activate.
Verification
New tests: TestResumeCanonicalSessionReturnsActiveCanonicalMirror (a),
TestCanonicalTakeoverOfSecondIdentityReturnsFirstMirror (b),
TestResumeLegacySessionReturnsActiveCanonicalMirror (c),
TestResumeCanonicalSessionReturnsLegacyMirrorReservation (legacy mirror
publishes its reservation), TestResumeCanonicalSessionHeldElsewhereKeepsMirror
(failure atomicity). Existing takeover, reclaim, resume, switch-recovery
and lease tests stay green; gofmt/vet/golangci-lint clean.
Co-authored-by: SivanCola <[email protected]>
Problem
After the desktop reclaimed a session, SIGHUP/SIGTERM no longer exited
the CLI: the process lingered as an orphan after an SSH drop and
systemctl stop had to escalate to SIGKILL, until the user typed /resume
or /takeover.
Root cause
shutdownAndQuit consumed every non-user tuiShutdownMsg while
sessionReclaimed or takeover.Returned() was set. That guard targeted a
stale exit the yield callback used to send, but the callback now sends
tuiSessionReclaimedMsg, so the only remaining senders were the signal
loop (no escalation) and the watchdog, whose requests were silently
dropped for the rest of the session.
Fix
Guard only while a reclaim transaction is actually in flight
(takeover.Reclaiming()), where the manager goroutine owns the final
snapshot and mirror return. Even then the request is deferred, not
dropped: completeSessionReclaim honors it as soon as the reclaim
callback lands. Once the session is reclaimed or returned, a signal
quits like any other exit and skips the shutdown snapshot, since the
runtime binding of that session is already released and the remote side
writes it.
Verification
TestTUIShutdownSignalQuitsAfterReclaim pins tea.Quit with zero
SnapshotForShutdown calls both after the callback and in the gap after
the return; TestTUIShutdownSignalDeferredWhileReclaiming keeps the
in-flight case and proves the deferred signal fires with the callback;
TestBubbleTeaKeepsRunningWhenShutdownRacesReclaim now checks the real
program loop survives the race and then exits gracefully on the
callback. TestCanonicalReclaimKeepsTUIAliveAndSwitchesSession and the
watchdog suite stay green; gofmt/vet/golangci-lint clean.
Co-authored-by: SivanCola <[email protected]>
Problem
Every /resume, picker open, /takeover <n>, and each keystroke completing
"/resume <n>" opened a session.Service for every other known project,
cached it for the process lifetime (never closed), and scheduled a
metadata rebuild for each unindexed catalog row there.
Root cause
otherProjectResumeEntries reused the current-workspace scan
(scanWorkspaceResume -> cliSessionService(root) -> Query().List) merely
to learn which legacy transcripts had been migrated, although the only
input that decision needs is the workspace's migration-map.json.
Fix
Split migration-map loading (readMigrationMapping, a plain file read)
from the index fold (migratedLegacyIndexWith, parameterized by a
"successor is live" predicate). The current workspace keeps its
"visible catalog row" rule; foreign workspaces use
foreignProjectResumeRows, which accepts a successor when its session
directory exists and never instantiates a service for that root.
Verification
New TestOtherProjectResumeRowsUseMigrationMapWithoutForeignService
registers a foreign project with a migrated and a live transcript and
asserts only the live one is offered and no service entry exists for the
foreign sessions-v4 root. TestResumeEntriesIncludeOtherProjects and the
merged-store tests still pass; gofmt/vet/golangci-lint clean.
Co-authored-by: SivanCola <[email protected]>
Problem
With more than 100 final-format sessions the newest conversation can be
missing from the --resume picker and /resume list, and --continue
(newestResumeTarget, scan.canonical[0]) can pick an older session.
Root cause
canonicalResumeEntries stopped paging once it had collected 100 visible
rows. The catalog pages in session-id (lexical) order, so the loop
returned the first 100 ids, and only then sorted those by UpdatedAt; the
newest session was never read when its id sorted after the first page.
Fix
Page the cursor through the catalog up to a documented hard cap
(canonicalResumeWalkCap = 20 pages of 100 rows), then sort by recency
and apply the display cap. The catalog dependency is expressed as a
small lister interface (*session.Query satisfies it) so the paging and
ranking contract is testable without hundreds of on-disk sessions.
Recency ordering from the owner would remove the walk entirely, but the
session package is outside this change's scope; the cap bounds the scan
until the catalog offers a recency index.
Verification
New TestCanonicalResumeEntriesRankNewestAcrossCatalogPages builds 250
sessions whose newest sorts last by id and asserts the newest 100 are
returned newest-first across 3 pages (fails on the old loop with
"newest row = s00099"); TestCanonicalResumeEntriesBoundTheCatalogWalk
pins the page bound. go test ./internal/cli/ -run
'TestCanonicalResumeEntries|TestMergedResumeEntries|TestNewestResumeTarget'
passes; gofmt/vet/golangci-lint clean.
Co-authored-by: SivanCola <[email protected]>
Problem
With ten or more legacy transcripts a desktop-created final-format
session never appears in /resume, the --resume picker, /rename <n>, or
/takeover <n>, even though it is the newest conversation.
Root cause
mergeResumeStores interleaves two newest-first lists but negated the
recency comparison: canonical rows older than or equal to a legacy run
were emitted before it while newer rows were deferred to the tail loop,
where capResumeEntries truncated them. scanWorkspaceResume also mutated
canonical ModTime through the migration borrow after
canonicalResumeEntries had sorted, so scan.canonical[0] (used by
--continue) was not guaranteed to be the newest row.
Fix
Emit a canonical row ahead of a legacy run exactly when it is newer than
the run's latest activity (ties keep the legacy row first so numeric
indices of an unchanged legacy list stay put), and re-sort scan.canonical
after the borrows so both the merge and newestResumeTarget see
newest-first input.
Verification
New TestMergeResumeStoresOrdersByRecency pins [L10,L5]+[C12] ->
[C12,L10,L5], [L10,L5]+[C7] -> [L10,C7,L5], older/tie/multi-row cases;
TestMergeResumeStoresCapKeepsNewestCanonicalRow proves the cap drops the
oldest legacy row rather than the newest canonical one. Both fail on the
inverted comparison and pass with the fix. go test ./internal/cli/ -run
'TestMergeResumeStores|TestMergedResumeEntries|TestNewestResumeTarget'
passes; gofmt/vet/golangci-lint clean.
Co-authored-by: SivanCola <[email protected]>
Problem
The required CI `lint` job fails on the PR: intrange in
session_takeover_canonical.go, staticcheck S1011 in resume.go (manual
append loop), QF1003 in resume_picker.go (if/else chain on one tag), and
a gofmt blank-line violation in resume_test.go.
Root cause
New code was written against older idioms the repository's golangci
configuration rejects; the test file was not gofmt'd.
Fix
Use `for range 2`, `append(out, base...)`, a tagged switch on the row
index, and gofmt the test file. Behavior is unchanged; the tagged switch
keeps the "active" status winning when both indices match.
Verification
gofmt -l internal/cli/ is empty; go vet ./internal/cli/ and
golangci-lint run ./internal/cli/ report 0 issues (the remaining
resume_catalog_test.go intrange items land with the ordering fix that
rewrites that file); go test ./internal/cli/ -run
'TestResumePicker|TestResumeEntries|TestCanonicalTakeoverCommand' passes.
Co-authored-by: SivanCola <[email protected]>
Problem
IsSnapshot was added by this PR with no caller anywhere in the
repository: internal/, cmd/, the desktop module and the frontend were
all swept and none reference it.
Root cause
A helper introduced for a call site that the final shape of the change
never needed.
Fix
Remove it. The package already exports IsContent for this question, and
IsContent is the stronger test every existing caller uses (internal/boot,
internal/agent, internal/provider/anthropic): it parses the block instead
of matching the opening tag as a prefix, so a truncated or malformed
block is correctly rejected. Keeping both would invite the weaker one to
be picked by a future caller. No provider-visible bytes change: the
removed function was never called and built no prompt content.
Verification
grep -rn IsSnapshot over *.go/*.ts/*.tsx/*.js/*.md (repo-wide, incl.
desktop/ and cmd/): no callers
go test ./internal/sessioncontext/ ./internal/provider/anthropic/ -run SessionContext
go vet ./internal/sessioncontext/ && golangci-lint run ./internal/sessioncontext/
Co-authored-by: SivanCola <[email protected]>
Problem
mirrorEndIdentity answered 204 the instant it saw the writer lock still
held, leaving the remote side read-only until the 30 s stale
auto-reclaim driven by /status polls noticed. The desktop sends
mirror-end at tab close and releases its runtime in the same teardown
with no ordering guarantee, so the common case was a farewell that
arrived microseconds before the drop and cost a 30 s recovery. The
identity branch of history() also lacked the "runtime changed during
read" re-check transcriptBoundRead performs.
Root cause
The farewell treated a held lock as terminal instead of as the expected
ordering, and the identity history read published a transcript captured
outside bindMu without re-resolving its route.
Fix
mirrorEndIdentity waits for the writer drop outside bindMu for a bounded
2 s (25 ms polls) and then reclaims through the existing locked path; a
writer that outlives the bound is still accepted, so the auto-reclaim
fallback and the mirror entry it needs are unchanged. The identity
history branch re-resolves the controller under bindMu after the read
and returns the same 409 as transcriptBoundRead when it changed.
Verification
go test -race ./internal/serve/ -run 'TestIdentityMirrorEnd|TestHistoryIdentityRoute|TestIdentityReclaim|TestAutoReclaim|TestMirrorEnd'
go vet ./internal/serve/ && golangci-lint run ./internal/serve/
Co-authored-by: SivanCola <[email protected]>
Problem
handoffIdentityLocked released the foreground without re-checking under
bindMu whether a turn had been admitted after the unlocked quiet probe.
POST /chat admitted in that window ran on a runtime the handoff then
unbound; service.Close returned ErrRuntimeBusy and the caller got a 500
with the foreground already rotated, no mirror entry and an orphaned
running runtime. Separately, serveHoldsIdentity, quietIdentityForHandoff,
ownershipIdentity and handoffIdentityLocked looked only at the
foreground, so an identity run by a detached background session was
reported as "other" and /adopt accepted a foreign claim over this
process's own writer.
Root cause
The identity path skipped the legacy handoffLocked invariants: the
locked busy re-check (turn admission holds bindMu, so an idle target
observed under the lock stays idle) and the detached-registry scan.
Fix
handoffIdentityLocked resolves the holder through identityHolderLocked
(foreground, else the detached session bound to the identity), returns
errHandoffBusyAgain when the holder reports work or the runtime is still
finalizing, and only then releases and closes; a close refused as busy
also maps to busy-again after the holder is re-attached. A detached
holder is taken from the watcher, released, and its controller retired
exactly like the legacy detached handoff. detachedIdentityHolder backs
every identity ownership predicate so /ownership answers "serve",
/adopt refuses, and /status does not pin a detached identity read-only.
A test hook runs between the quiet probe and the lock.
Verification
go test -race ./internal/serve/ -run 'TestIdentity|TestHandoff|TestReclaim|TestMirrorEnd|TestAdopt|TestOwnership|TestExclusiveV3'
go vet ./internal/serve/ && golangci-lint run ./internal/serve/
Co-authored-by: SivanCola <[email protected]>
Problem
Every identity /handoff rotated the foreground with BindFreshSession,
which runs PrepareCreate, seeds, flushes and publishes a brand-new
canonical session. Each handoff/reclaim cycle therefore left an empty
row in /sessions, whereas the legacy SessionLeaseKeeper.ReleaseForHandoff
only unbinds authority.
Root cause
The controller had no primitive to release a bound identity without
allocating a successor, and NewSession failed outright on an exclusive
controller with no bound runtime, so the host rotated eagerly to keep
the foreground usable.
Fix
control.Controller.ReleaseSessionForHandoff flushes the runtime, drops
the client binding, empties the in-memory transcript and rebinds the
turn-event state to "no store", leaving the controller in the same
never-bound exclusive state a fresh controller starts in; the next turn
allocates lazily through ensureWriteAuthorityReady, and
rotateExclusiveSession now allocates on demand when nothing is bound.
Serve's handoffIdentityLocked uses the primitive, closes the runtime to
drop the writer lock, and re-attaches the foreground through OpenSession
if that close is refused, so a failed release never leaves the writer
held by an unbound foreground. The turn_events.go gofmt drift from the
PR is folded in.
Verification
go test ./internal/control/ -run 'TestReleaseSessionForHandoffLeavesControllerAllocatable|TestOpenSessionReopensRuntimeClosedByReclaim'
go test ./internal/serve/ -run 'TestIdentity|TestHandoff|TestReclaim|TestMirrorEnd|TestAdopt|TestExclusiveV3'
gofmt -l, go vet and golangci-lint run on internal/control and internal/serve
Co-authored-by: SivanCola <[email protected]>
Problem
session.ProbeWriterHeld answered "who holds this identity" by taking
writer.lock exclusively (LOCK_EX|LOCK_NB plus the in-process exclusive
slot) for an instant. A legitimate acquireSessionWriter landing in that
instant (the desktop opening after /handoff, or serve's own OpenSession
in reclaim racing a /status poll) saw ErrHeld and surfaced
ErrWriterOwned for a session nobody owned. The probe ran twice per
/status poll on identity routes and five times a second in the reclaim
loop, and created writer.lock as a side effect.
Root cause
The probe and the acquirer shared one lock with no way to tell a
transient probe hold from a real owner, and this process had no record
of the writer leases it already holds.
Fix
directory_ownership.go now owns a process-local writer registry plus a
mutex that makes an in-process probe and an in-process acquire mutually
exclusive, so that collision is impossible rather than retried. The
probe answers from the registry when this process holds the lease and
never touches the lock file; a never-created writer.lock counts as free
without creating it. For a probe in another process, the acquirer
tolerates ErrHeld on the inner writer.lock with a bounded, documented
retry (5 x 10 ms); the outer directory claim is never probed, so ErrHeld
there is still refused at once. Desktop and CLI acquirers share this
code through internal/session.
Verification
go test -race ./internal/session/ -run 'TestProbeWriterHeld|TestAcquireSessionWriter|TestStorageRevisionExternalLease|TestPurge|Maintenance'
go vet ./internal/session/ && golangci-lint run ./internal/session/
Co-authored-by: SivanCola <[email protected]>
Problem
Broadcaster.Emit and EmitTo still canonicalized SessionPath as a
filesystem path, so a "session-id:<id>" route became "<cwd>/session-id:
<id>". The taken-over, reclaim-requested, reclaimed and adopted notices
for final-format identities therefore carried a mangled route, were
marked SessionCurrent=false and were dropped for current-only (browser)
subscribers. Only EmitWire had been taught about identity routes.
Root cause
Route normalization was repeated per emit path instead of living once
in the broadcaster, and the current-only filter compared an identity
route against a path key it can never equal.
Fix
sessionRouteKey is the single normalization rule for every broadcaster
key (Emit, EmitTo, EmitWire, SetCurrentSession, ResetSessionPath and the
usage ledger): identity routes stay verbatim, legacy paths canonicalize.
hiddenFromCurrentOnly applies one filter rule in Emit and EmitTo:
identity routes are not path-keyed and pass to current-only subscribers
exactly like identity-tagged live frames already do. mirrorKey now
delegates to the same rule so registry keys and emitted routes agree.
Verification
go test ./internal/serve/ -run 'TestBroadcaster|TestExternalFrames|TestMirrorGeneration|TestIdentity|TestReclaim|TestAdopt|TestHandoff|TestOwnership'
go vet ./internal/serve/ && golangci-lint run ./internal/serve/
Co-authored-by: SivanCola <[email protected]>
Problem
Deleting a canonical session through POST /delete-session removed the
legacy transcript recorded as its migration source without checking
whether another canonical row still referenced it, and without any of
the ownership or containment guards the legacy delete applies. A source
that /sessions still lists as a distinct session (two targets), a source
a local window is writing, or a path outside the session dir could all
be deleted with their sidecars. The lookup also called Close() on the
shared session query, stopping its background rebuilds.
Root cause
Listing and deletion used two different migration-index rules:
loadMigrationIndex hid a source only when it had exactly one live
target, while migrationSourceForTarget returned the first entry whose
TargetID matched and passed the recorded path straight to
removeSessionFiles.
Fix
One shared loadMigrationIndex(roots, exists) builds the sole-target
index for both /sessions and the canonical delete. The delete resolves
the source before the catalog row disappears (so the row still counts as
a live target), validates it with resolveSessionPath (session-dir
containment, transcript name, cleanup marker) and the extracted
legacyTranscriptDeleteRefusalLocked gate (active, detached, mirrored),
and removes it through destroyLegacyTranscriptLocked, the same teardown
the named legacy delete uses. A refused source fails the request before
anything is removed; an unresolvable source is left alone and logged.
Verification
go test ./internal/serve/ -run 'TestDeleteCanonicalSession|TestDeleteSession|TestSessions|TestExclusiveV3'
go vet ./internal/serve/ && golangci-lint run ./internal/serve/
Co-authored-by: SivanCola <[email protected]>
Problem
`pnpm check:bundle` failed on the merged tree: the initial raw payload is
2022.6 KiB against the branch's 2020.5 KiB ceiling. The ledger comment above
that ceiling also mixed measurements from two different scales (2437.x KiB
lines beside 206x xxx B lines), and the PR had deleted two main-v2 entries,
so the recorded history no longer explained the value.
Root cause
The 2020.5 ceiling was carried over from an earlier point of the branch and
left only 0.2 KiB of headroom, which this review series' fixes consume. The
2437.x lines predate the chunking change that moved the initial payload from
~2.44 MiB to ~2.02 MiB, so they describe a payload that no longer exists.
Fix
Restore the two main-v2 ledger entries the PR removed, drop the stale
mixed-scale lines, and record one paragraph with the measured bytes and the
attributable cause: 2071101 B, 5952 B (0.288%) over main-v2's recorded
2065149 B, driven by this branch's remote session identity work (remote
telemetry and status become static imports of useRemoteSession and leave
their lazy surface chunk, plus the canonical session-id plumbing, spectator
reconcile loop and ownership classification, the pre-activation history
prime, the rebased-submission settlement and the tree's canonical row
identity). Set the ceiling to the next one-decimal value, 2022.7 KiB,
keeping 0.14 KiB headroom in line with the recent 0.15 KiB precedent. The
gzip, CSS and chunk limits are untouched and still pass.
Verification
pnpm build:electron && pnpm check:bundle: initial raw JavaScript and CSS
2022.6 KiB / 2022.7 KiB, largest initial chunk 518.8 KiB / 1000.0 KiB, all
gzip limits passing.
Co-authored-by: SivanCola <[email protected]>
Problem
With the Serve moving its synthetic "current" row to a real `sessionId`,
clicking that row in browser/dev mock mode opened the workspace surface but
bound no session: the tab kept the previously synthesized basename route,
no row became current, and TabMeta carried no `sessionId`, so the runtime
identity key fell back to the tab/topic key.
Root cause
`mockRemoteProjects.OpenRemoteProjectTab` only honoured `opts.sessionName`.
The real bridge already prefers `opts.SessionID` over the path and name
(desktop/remote_projects.go, remote_tab_multisession.go) and publishes it as
TabMeta.SessionID, so the mock had drifted from the contract it implements.
The click path itself is correct: `openRemoteSessionNode` forwards
`sessionId` with no path or name for a canonical row (pinned by the tree
test added earlier in this series).
Fix
Resolve the requested session by canonical identity first (`sessionId`, else
the legacy name), bind `tab.sessionId`, adopt the row's durable path when it
has one instead of synthesizing a basename route, and mark that row current.
Legacy rows keep their existing basename behaviour unchanged.
Verification
pnpm typecheck; pnpm test:typecheck; mock-remote-catalog.test.ts (extended:
canonical open binds the id and drops the stale legacy route, legacy open
keeps its basename route and becomes the current row);
remote-tab-opened.test.tsx; add-project-entries.test.ts;
remote-session-cache.test.ts; statusbar-workspace.test.tsx.
Co-authored-by: SivanCola <[email protected]>
Problem
`buildSidebarRegionProps` built `activeRemote = { ...remote, sessionId }`
inline during the AppRuntimeView render, which runs on every transcript
delta. The project tree keys ancestor expansion, active-row marking, the
read-activity effect and its whole row projection on that value, so each
streaming delta invalidated those memos and re-ran full tree walks over the
project/session tree.
Root cause
The PR introduced the composed reference (host, workspace and the bound
session id) inside a pure prop-assembly function, which by contract owns no
state and therefore cannot memoize. The reference had no owner.
Fix
Add `useActiveRemoteRef`, which owns the value and rebuilds it only when the
host, workspace or bound session id changes. It keys on those primitives
rather than on `tab.remote`'s object identity, because RemoteTabRef carries
exactly hostId and workspace (desktop/remote_projects.go): an unrelated
tab-meta refresh (running flag, turn counters) replaces the nested object
but must not churn the sidebar. AppRuntimeView calls the hook and passes the
result through the assembly, which now forwards it untouched; the input is
required so a future caller cannot silently drop it.
Verification
pnpm typecheck; pnpm test:typecheck; check-app-layers;
sidebar-active-remote-identity.test.tsx (new: identity stable across
re-renders and unrelated meta refreshes, new reference on session rotation
and host change, undefined for local tabs and drafts, forwarded by identity
through the assembly); independent-session-boundaries.test.ts;
remote-project-tree.test.tsx.
Co-authored-by: SivanCola <[email protected]>
Problem
Switching remote tabs A -> B -> A reverted A's in-flight composer choice:
the mode, approval, goal or quality-floor the user had just picked snapped
back to the backend value until the next status refresh.
Root cause
`useRemoteComposerProfileSync` kept a single `lastRouteRef` holding only the
most recently synced route, while its `routeKey` includes `activeTabId`.
Every tab switch therefore compared A's key against B's and reported a route
change, which passes `undefined` as the existing profile to
`reconcileComposerProfile` and drops `pending: {}` — exactly the path meant
for a real session rotation.
Fix
Replace the single ref with a per-tab `Map` of the last synced session
route. A tab is only a rotation when that tab's own route changed; a tab
seen for the first time reconciles over whatever the profile table already
holds, as before. Entries for tabs the profile table no longer contains are
pruned on the next sync, so the memory stays bounded to live tabs and a tab
id reused after removal reads as a first sync rather than a rotation.
Verification
pnpm typecheck; pnpm test:typecheck; remote-composer-profile-sync.test.tsx
(new: A -> B -> A preserves A's pending flags and its visible mode, B stays
untouched, A's own route change adopts the backend profile, and a forgotten
tab id is not a rotation but still rotates on its next real change);
remote-composer-presentation.test.tsx; remote-composer-commands.test.tsx;
controller-profile-lifecycle.test.tsx.
Co-authored-by: SivanCola <[email protected]>
Problem
Once remote-session-surface.test.tsx ran to completion, three hydration
race/rotation checks failed: "a ready generation re-hydrates after
discarding the stale in-flight snapshot", "ready-to-ready rotation hydrates
the adopted session while old reconciliation is pending" and "session
generation fence rejects stale history and hands reconciliation to the new
generation".
Root cause
Those scenarios count RemoteTabSnapshot calls (deferred resolvers indexed
by call order). The PR's pre-activation history prime issues one canonical
window read at mount. The harness mounts useRemoteSession without a
controller, so `identityFor(tabId)` resolves to "local" and the read goes
to the mock reader's SessionHistoryWindowForTab, whose mockReaderSlice
feeds from the fixture's HistoryForTab -> RemoteTabSnapshot. The prime
therefore consumed a scenario deferred and shifted every index. This was
masked at PR head because the suite aborted earlier.
Fix
Declare the remote binding identity in the harness
(`setTranscriptBindingIdentity(() => "remote")`), which is what the
controller does for every remote tab in production, and stub
RemoteSessionHistoryWindowForTab like a serve whose tab has not attached
yet. The prime stays inert and the mock Follow path (local window) is
untouched, so the scenarios again count only Follow-driven snapshots.
Verification
remote-session-surface.test.tsx: 115 passed, 0 failed.
Co-authored-by: SivanCola <[email protected]>
Problem
useRemoteSession's hydrate() treated every `follower.start()` or status
failure as a take-over: a transient network error or a restarting serve
retried the legacy status snapshot up to 60x500 ms, then installed the
protocol-1 history view and reported `state="ready"`, `hydrated=true`,
`error=""`. From there submit() threw "Transcript v2 is not synchronized"
and nothing re-ran hydrate(): the spectator poll only flipped `spectator`
back to false. When the fallback itself failed, the error escaped the catch
block as an unhandled rejection (remote-session-surface.test.tsx aborted on
"history exceeds bridge limit").
Root cause
The catch block did not discriminate the failure. The serve's take-over
surface is specific: a spectator's Follow request fails with "remote
transcript read failed (HTTP 409)" (desktop/remote_transcript_api.go) and
status/meta carry `takenOver`. The fallback also ran outside any try, and
ownership returning had no owner: `spectator` was written from two places
(status, tab meta) but no code observed the true -> false transition.
Fix
Add `isRemoteTakeoverError` (mirrors remoteSessionTakenOver's wordings plus
the HTTP 409 surface) and `remoteStatusTakenOver`. hydrate() now falls back
only when the error is a take-over or a one-shot status probe reports
`takenOver`; every other failure keeps `setError` as before, and a failing
fallback surfaces its own error instead of rejecting hydrate(). Ownership
observations funnel through one `noteOwnership` seam used by both the status
and meta paths; it mirrors the last verdict in a ref and re-runs hydrate()
on the spectator -> owner transition, so the follower re-attaches and the
composer can send again. The legacy install retires the early history prime
(it owns the surface from then on). The observation resets per mounted tab.
Verification
pnpm typecheck; pnpm test:typecheck; remote-session-takeover-hydration
(new, 13 checks: transient error surfaced and no legacy install; 409 falls
back to the legacy view; spectator submit refused; owner return via tab meta
and via the status poll re-attaches exactly once; status verdict admits the
fallback for a non-409 failure; fresh tab hydrates once). remote-session-
surface.test.tsx now runs to completion (previously aborted); its three
remaining race/rotation failures are a harness coupling with the history
prime, fixed in the following commit.
Co-authored-by: SivanCola <[email protected]>
Problem
useRemoteSession's pre-activation history prime mutated the resident
transcript store before its "blank transcript" guard ran, and every
`remote-tab:updated` publication retried it while `hydratedRef` was false.
A late legacy `/history` window could therefore replace records and cursors
the follower had already installed, and each retry bumped the resident
session generation, retiring the follower's in-flight reads.
Root cause
`TranscriptStore.loadLatest` bumps `session.generation` and calls
`replaceRecords` before it consults the caller's `current` predicate, so the
PR's guard (placed after `await loadLatest`) ran only after the damage. The
prime also keyed its lifetime on `hydratedRef`, which flips only after the
status snapshot completes, long after the follower's install cut has become
the store owner. Two further latent defects: the prime's `current` predicate
compared against the hydrate generation, which `hydrate()` bumps
synchronously right after the mount-time prime starts, so the mount-time
prime was always discarded; and two independent booleans
(`primedRef`/`primeInFlightRef`) encoded a four-state lifecycle.
Fix
Replace the booleans with one named substate `primeRef: idle | loading |
primed | retired`. Check for existing content (items or a live tail) before
calling `loadLatest`, marking the prime retired without touching the store.
Retire the prime when the follower dispatches its install cut
(`transcript_v2_snapshot`; connection frames precede it and own nothing), so
a window landing afterwards is dropped by `loadLatest`'s `current` predicate
before `replaceRecords`, and later attach publications are no-ops. Scope the
prime to the mounted identity (`cancelled`) and store ownership instead of
the hydrate generation, so a stalled or failed follower still leaves the
durable baseline visible; the follower cut supersedes it when it arrives.
Verification
pnpm typecheck; pnpm test:typecheck; remote-session-history-prime.test.tsx
(new, 11 checks: late window cannot replace transcript or store records,
generation unchanged, retries no-op, resident content skips the read,
prime-first then follower supersedes); remote-tab-opened.test.tsx;
remote-fork-targets.test.tsx.
Co-authored-by: SivanCola <[email protected]>
Problem
`pendingTurnRef` in useRemoteSession.ts is assigned on mount reset, on the
ready republication and in submit(), but never read. The PR's ready-state
hunk added a comment claiming the assignment clears a stale optimistic
pending submission and prevents a zombie "processing" indicator; the
assignment has no such effect, so the comment misleads readers into
believing a reconciliation exists.
Root cause
The ref predates the transcript-store ownership of pending submissions
(`localSubmissions`/`pendingSubmissionId` live in reducer state, settled by
localSubmissionState) and was left behind as dead state. The PR built on it
instead of on the reducer state, which is the boundary the previous commit
fixes.
Fix
Delete the ref, its three writes and the misleading comment. The ready
handler again simply re-hydrates; stale echoes are settled by the reducer's
snapshot path.
Verification
pnpm typecheck; remote-tab-opened.test.tsx; remote-fork-targets.test.tsx;
conversation-projection.test.ts.
Co-authored-by: SivanCola <[email protected]>
Problem
After a snapshot rebase (runtime rebuild, serve restart, model switch) a
submission the server had already journaled kept rendering as a second user
bubble stuck at "processing". The PR's regression suite failed 5 of 11
checks and the zombie indicator was not fixed.
Root cause
The PR added its absorption inside transcriptSnapshotState by filtering
`state.items` for user items carrying the pending submission id. Optimistic
submissions never live in `items`: `startLocalSubmission` records them in
`localSubmissions`/`localSubmissionOrder` and the chat view projects them
from there. The filter was therefore always empty and `running`,
`pendingSubmissionId` and the duplicate bubble stayed governed by the
untouched `hasLocalSubmission` path. Two of the PR's checks also looked for
the echo in `state.items`, i.e. at the wrong seam.
Fix
Add `settleRebasedSubmissions` next to `settleLocalSubmissions` in
localSubmissionState.ts, the owner of echo lifetime, and call it from
transcriptSnapshotState after the id/messageId settlement. It retires an
echo when a durable record repeats its submission id (no messageId needed)
and, only while the runtime is idle, the oldest echo whose text the newest
durable user record repeats. The text fallback is bounded by the echo's own
anchor: durable copies must outnumber the identical user items that were
already on screen when the echo was created, so an earlier identical message
can never absorb a newer lost re-send (the review nit). An active runtime
never absorbs by text. The PR's dead `optimistic`/`represented` code is
removed and `running`/`cancellable`/`pendingUser` return to main-v2's form,
now settling naturally because the absorbed echo is gone.
The test measures the echo where the user sees it (durable items plus
unmatched local submissions, mirroring chatViewSource) instead of
`state.items`, and adds the id-only link, the confirmed-echo duplicate, the
identical-message bound, and its release once the second copy is journaled.
Verification
pnpm typecheck; pnpm test:typecheck; transcript-snapshot-optimistic-orphan
(17 checks); submission-handoff (25); transcript-session-follower (27);
transcript-store (367); history-preparation; remote-running-reconcile (18);
session-submission-lifecycle. remote-session-surface.test.tsx still aborts
at PR head on an unhandled rejection thrown from useRemoteSession's legacy
hydration fallback (the PR's catch block re-throws); that is fixed with the
take-over gating commit, after which the suite is re-run.
Co-authored-by: SivanCola <[email protected]>
Problem
Two canonical remote sessions opened in the same remote tab produced the
same runtime identity key, so the session fence, resource operations and
controller profile targets could not tell them apart. The PR's regression
check in session-control-commands.test.ts failed (both inputs keyed to
`topic\0\0\0\0remote`) and did not compile under tsconfig.test.json.
Root cause
`sessionIdentityKey` derives the canonical key from `identity.session`
(a SessionRef). Remote tab metas never carry a SessionRef: desktop's
remoteTabMetaLocked publishes the canonical id only as the top-level
`sessionId` compatibility field (local metas build the SessionRef from it in
tabs.go). The frontend therefore fell through to the topic/tab fallback for
every remote tab, and the fallback was only unique by accident of the Go
topic id embedding the session id.
Fix
Make `sessionIdentityKey` the owner of the invariant: `SessionIdentityInput`
accepts the TabMeta compatibility fields (`sessionId`, `remote.hostId`) and,
when no SessionRef is present, keys on `ref\0<host>\0<sessionId>`. The
generation is deliberately excluded on this path because a remote tab's
`sessionGeneration` is its event-pump reconnect counter (remote_tab_publication.go),
not a session identity; including it would re-fence every reconnect. All
three callers (AppRuntime, controllerProfileOwner) pass the new fields.
The test keeps the real TabMeta shape (top-level sessionId) and now also
carries the remote host; it additionally asserts that a generation bump does
not change identity, that the same id on two hosts differs, and that a
host-less compatibility id still keys the session rather than the tab.
Verification
pnpm typecheck; pnpm test:typecheck; session-control-commands.test.ts;
session-identity.test.ts; independent-session-boundaries.test.ts;
navigation-surface-lifecycle.test.tsx; remote-composer-commands.test.tsx;
controller-profile-lifecycle.test.tsx; active-tab-mirror.test.tsx.
Co-authored-by: SivanCola <[email protected]>
Problem
remote-project-tree.test.tsx reported 24 pass / 1 fail: its source regex
still required the `remoteSession` mapping to end at `title: row.title }`
while ProjectTreeRemoteGroups.tsx now also carries `current: row.current`.
The suite also had no functional check that a sessionId-only synthetic
"current" row forwards its id to OpenRemoteProjectTab.
Root cause
The PR extended the row mapping (the `current` flag drives the trash guard
for the not-yet-catalogued session) without updating the contract regex, and
the Go side is moving the synthetic current row from a `session-id:` path to
a real `sessionId`, so the click path needed an explicit forwarding test.
Fix
Tighten the regex to the full mapping including `current: row.current`
instead of relaxing it. Add a functional case: a row with only `sessionId`
merges without a synthesized path, `openRemoteSessionNode` forwards
`sessionId` (no `sessionPath`/`sessionName`) to the open port, and
`remoteSessionActionIdentity` addresses the row by id, decoding the legacy
`session-id:` path only as a fallback.
Verification
tsx src/__tests__/remote-project-tree.test.tsx (28 passed).
Co-authored-by: SivanCola <[email protected]>
Problem
The merged tree fails `pnpm typecheck`: AppRuntimeView.tsx references
`workspaceContextTab`, a name that no longer exists after the merge took
main-v2's shape of the composer workspace-context block.
Root cause
The PR renamed the workspace-context source tab to `workspaceContextTab`
while main-v2 (da9adbd05) already isolates the draft workspace context in
decisionFooterBuilders and keeps `activeTab` as the only tab in scope. The
merge resolved the surrounding block to main-v2's form but kept the PR's
identifier on the `remote` line.
Fix
Use `remote: Boolean(activeTab?.remote)`, matching main-v2 and the sibling
`gitBranch`/`workspaceName` lines that already read `activeTab`. The PR's
takeover-aware terminal `readOnly` predicate is unchanged.
Verification
pnpm typecheck; composer-workspace-context.test.tsx; workspace-layout.test.ts.
Co-authored-by: SivanCola <[email protected]>
Conflict resolutions:
- desktop/app.go: keep main-v2; e7a5b149e moved AttachDropped and its
helpers to attachments_app.go, so the branch's edit to that block is
obsolete.
- desktop/frontend/src/app-shell/AppRuntimeView.tsx: keep main-v2;
da9adbd05 already isolates the draft workspace context in
decisionFooterBuilders, making the branch's draft-aware hunks duplicates.
- internal/serve/session_ownership.go: keep the session import only; the
store import became unused when 08ca31641 moved resolveSessionPath.
- desktop/frontend/scripts/check-bundle-budget.mjs: keep the branch ceiling
pending a re-measurement of the merged build.
Co-authored-by: SivanCola <[email protected]>
SDDL renders well-known accounts as aliases (the CI runner's
administrator shows as LA), so the reset test compares the ACE trustee
SID and the protected control bit instead of the rendered string. The
Windows-tagged lint run also asked for strings.CutPrefix.
Windows CI showed the WRITE_DAC-only reset failing with access denied:
CreateFile silently adds SYNCHRONIZE and FILE_READ_ATTRIBUTES to every
open, and both rights are inside the legacy DENY RX mask, so the Win32
open could never succeed against the very file the reset exists for.
The same applies to MoveFileEx behind os.Rename.
Route both operations through NtCreateFile with the exact access mask
(WRITE_DAC for the DACL reset, DELETE for the quarantine rename via
FileRenameInformation), opening reparse points as themselves. The
quarantine now uses that rename so a read deny cannot block it either.
A credential store locked by a deny entry could only be recovered when a
retired-sandbox marker proved the entry came from Reasonix and the
process token owned the file; otherwise the repair's own DACL read
failed and its error replaced the original one, so users could neither
read nor re-enter their API key.
Plain reads keep the original permission error and attach the repair
outcome as a wrapped cause. Explicit saves gain a recovery ladder in the
config package: after the provenance-checked repair, reset the file's
DACL to a protected current-user grant through a WRITE_DAC-only handle
that never reads the existing descriptor, and if that is denied too,
move the locked file aside as .env.locked-<timestamp> and write a fresh
store. The reset lives in winaclresidue and is reserved for the global
credential file; both steps log what they did.
The Windows branch still encoded the retired shell policy that rejected
Bash before launch. Without a backend, Windows takes the same unwrapped
path as a Linux host without bubblewrap.
The first baseline refresh ratcheted every budget to the branch's exact
measurements, so the PR merge with main-v2 (desktop/app.go grew by one
line in #10558) failed repolint. Drop only the entries for deleted files
and keep the previous budgets and repo ceilings for everything else.
Windows no longer enforces an OS-level shell sandbox, so the
restricted-token and AppContainer backend in internal/winsandbox, the
hidden helper subcommand dispatched by the CLI and Desktop entry points,
the helper report channel, and the Spec fields only that backend read
are deleted. Callers that classified helper failures or issued
runner-authorization denial hints are simplified to the ordinary
process-launch outcomes.
The one part older installs still depend on moves to a leaf package,
internal/winaclresidue: the conservative legacy credential DENY RX
repair and a sweep of the crash-residue markers that v1.38.8 through
v1.38.10 wrote before mutating ACLs. The sweep now runs in the
background at process start instead of only before the next sandboxed
command, and a marker is deleted only after every recorded ACE removal
succeeded so a failed cleanup keeps its evidence. The repair no longer
takes the sandbox root lock; live-owner markers still veto it.
CI drops the sandbox wait knob and points the Windows credential ACL
identity step at the new package. The Windows sandbox architecture
document, its evidence directory, and the native verification script
are removed with the backend.
The visible-indicator count still raced on CI runners after the first
deterministic rewrite. Poll until no indicator is rendered, bounded, and
put the surviving indicators' labels into the failure message so a real
regression identifies the row that stayed lit.
Problem: sending from a draft over an open session created the new session
in the background while the surface stayed on the old one, and clicking a
session row after the last visible session was archived failed. A service
built with version "dev" (pnpm dev:desktop) or with desktop telemetry off
could not complete the shell handshake.
Root cause: OpenSession installed the target into the active tab and
returned "workspace is not ready" when none existed; draft acceptance never
activates the tab it creates, and the removed replacement blank session had
been masking the gap. HelloResult.RunID/IncidentID carried omitempty, so a
service without a lifecycle tracker dropped keys the shell validator
requires to be present (empty allowed).
Fix: surfaceForCanonicalSession resolves the surface for OpenSession (active
tab, else a visible tab already running the session, else an empty surface
created for the session's workspace and bound by the resume path, discarded
if the bind fails). The hello identifiers are always serialized; the
protocol document states they are empty when diagnostics are disabled.
Verification: desktop go test ./..., hostrpc tests, Electron shell tests
and typecheck, and a live dev-version shell run driven over CDP.
The "last job completion clears project activity" check waited for the
activity indicator to become hidden and then required zero matching
elements in the whole document. Folder rows can hold a fallback indicator
that is not visible at that instant, so the count raced with sidebar
updates and failed twice on CI for a copy-only change while passing
locally and on main-v2.
Wait until every indicator has no rendered box, then assert that no
visible indicator remains. A stale visible indicator still fails the
wait, so the user-facing invariant is unchanged.
Archiving or trashing the last visible session ran openFallbackRuntime,
which synthesized a "transient" blank session. The canonical registry now
publishes that blank as a real sidebar row, so "新的会话" reappeared after
every archive, each attempt left a desktop-* entry in the trash, and the
workspace could never become empty. The draft landing already owns the
empty state, so the fallback only duplicated it.
- openFallbackRuntime only re-activates a topic that still owns content;
with nothing left, zero tabs stand and the frontend lands on the draft of
the workspace the user was in instead of always Global.
- archiveSessionRefs* no longer return a fallback target; the transient
blank runtime path is removed.
- The workspace draft badge marks unsent content or an unsettled save; a
clean empty draft is only the landing surface. HasContent is derived from
the content fields, since an emptied composer saves a full blank object.
Regressions cover archiving the last visible session, trashing the last
topic, the legacy delete path, draft content detection, the landing target,
and the badge rule.
Windows has no OS-level shell sandbox any more. The restricted-token and
AppContainer backend re-introduced by #10209 implemented forbid_read as a
temporary DENY RX ACE on the current user's own SID, which locked the host
out of its credential store during every sandboxed command and permanently
after a crash, stop, or silently failed cleanup (#10494, #10496, #10531,
#10541). The same lane failed closed on unreachable write roots (#10505),
demoted the persistent PowerShell on cold hosts (#10534), ignored
bash = "off" (#10292), and refused Bash/MSYS2 (#10523).
Forcing the config default to off was not sufficient because the preset
mapping in specForCall re-enabled enforcement per preset. The decision now
lives at the platform-capability boundary: sandbox.OSSandboxSupported()
is false on Windows, sandbox.Available() is always false there, config
normalization resolves every [sandbox].bash value to off, the preset
mapping never demands enforce, and the capability snapshot reports no
backend while keeping every preset selectable. No Windows launch path
wraps a command, so the helper never mutates ACLs or creates restricted
tokens; restricted presets remain Reasonix tool-layer boundaries and
shell commands run as the current OS user.
Desktop copy gains Windows variants for the Workspace-write description
and the sandbox hint, and the shell network toggle is inert on Windows.
Docs describe the retired backend; WINDOWS_SANDBOX.md keeps the design
as a historical record until the code is removed. Windows-only tests
that exercised the retired helper are replaced by contract tests that
assert no launch path wraps and no preset demands confinement.
A canonical session had three name stores with different owners: the
session log, the workspace presentation row, and the legacy topic map. The
sidebar fell back to the first user turn, the topicbar bind fell back to
the presentation default, and a restored or rebuilt tab read only the
topic map, which no canonical rename ever wrote. AI rename updated the
live tab in memory, so the name looked right until the tab was rebuilt.
- sessionDisplayTitle is the single chain for canonical sessions: log
title, migrated presentation title, first user turn, default. The
sidebar rows and every tab bind use it.
- Controller builds re-derive the tab name from the log after binding and
seed a manual topic name chosen before any session existed into the
presentation row.
- publishCanonicalSessionTitle is the single exit of every canonical
rename: it mirrors the log into the presentation row, open tabs, the
prompt-history cache, and the project tree.
- Legacy renames now project the custom title into the open tab as well.
Regression tests cover an untitled session, rename followed by switch and
restart rebuild, a cleared title, and the legacy path.
Incorporate main-v2 through 31d84501e, including the restored tab identity extraction. The upstream extraction reduces restoreOrBuildTabs below the checked function-size baseline, so restore the baseline from the temporary 372 normalization to 368.
Problem: the previous main-v2 integration temporarily required a four-line desktop/app.go baseline carry-forward.
Root cause: main-v2 advanced again while CI ran and now extracts restored-tab identity setup, removing those excess lines.
Fix: restore the original 368 per-file function-size baseline so the PR does not widen the lint allowance.
Verification: go run ./tools/repolint passes with 1230 baselined findings on main-v2 at 31d84501e.
Integrate main-v2 at 31d84501e, which extracts restored-tab identity initialization from desktop/app.go and restores the existing repolint budget.
The approval file-reference and animation regressions, repolint, and the Desktop exact prompt race test pass on the combined tree.
Problem: lint-code fails after integrating current main-v2 because desktop/app.go reports 372 excess function lines against its recorded 368 baseline.
Root cause: already merged session routing changes increased that per-file metric while the baseline remained at the prior value; this PR does not otherwise change desktop/app.go.
Fix: carry forward only the four-line per-file function-size delta. Repository-wide limits and every other file baseline remain unchanged.
Verification: go run ./tools/repolint passes with 1231 baselined findings.
Problem:
The app-settings-motion browser group intermittently failed after the Composer
showed two background jobs because the test immediately sampled the project
tree's activity indicator.
Root cause:
The Composer run strip and project tree are separate React consumers. Waiting
for one consumer does not guarantee that the other has committed the same
runtime frame under CI load.
Fix:
Wait directly for the animated project activity indicator to appear and for
all activity indicators to disappear before asserting both ownership states.
Verification:
- node bench/runtime-state.mjs
- go run ./tools/repolint
Problem:
The latest main-v2 increases desktop/app.go's function-size weight from 368
to 372, so repolint fails on an unchanged base checkout.
Root cause:
The merged session route ownership work added four weighted function-size
units without updating the checked-in baseline.
Fix:
Carry forward exactly the four units already present on main-v2. No source
limit or unrelated lint budget is changed.
Verification:
- reproduced on a detached eeb643911 main-v2 worktree
- go run ./tools/repolint
Problem: the latest main-v2 integration measures 2065149 bytes of initial raw JavaScript and CSS, 151 bytes above the previous budget.
Root cause: the bounded approval outcome and cancellation ownership wiring adds about 1.1 KiB over the same-toolchain main-v2 baseline of 2015.6 KiB.
Fix: move only the initial raw payload ratchet to 2016.9 KiB, retaining about 0.15 KiB headroom while leaving gzip, CSS, and chunk budgets unchanged.
Verification: the production build completed compilation and bundling; the exact generated assets pass every bundle budget with the new ratchet.
Integrate session routing, Windows workspace ownership, and transcript identity changes from main-v2 at eeb643911.
The upstream changes do not overlap the approval UI files. Focused approval, cancellation, frontend typecheck, and Go exact-approval tests pass on the combined tree.
Problem: Desktop frontend CI lost the current plan revision file suggestions when the workspace scope changed.
Root cause: the approval component key included workspaceScopeKey, so the same prompt was remounted while its scope-specific file request was in flight.
Fix: key local decision state only by the prompt and tab identity; the file reference owner continues to fence and refresh requests by workspace scope.
Verification: approval-modal-file-reference (59 assertions), approval-animation (27 assertions), and frontend test typecheck passed.
Problem:
Windows boot tests and diagnostic inventory still assumed the provider-visible shell was bash.
Root cause:
The schema migration registered pwsh at runtime, but shared boot expectations and KnownToolNames retained the POSIX-only identity.
Fix:
Centralize the platform shell expectation in boot tests, keep legacy bash in skill fixtures as compatibility input, and register pwsh as a known diagnostic identity.
Verification:
Focused boot contract tests, tool tests, boot/tool go vet, Windows amd64 boot/tool test-binary builds, repolint, and golangci-lint v2.12.2 pass.
Problem:
The Windows control suite still expected RunShell to dispatch under the legacy bash tool name.
Root cause:
The cross-platform event test hard-coded the POSIX provider identity after Windows moved to pwsh.
Fix:
Assert the platform contract: pwsh on Windows and bash elsewhere.
Verification:
The focused control test, control go vet, Windows amd64 control test-binary build, and golangci-lint v2.12.2 pass.
Problem:
- The Windows Desktop golangci-lint leg rejected an assignment that was always
overwritten before use.
Root cause:
- EnsureWorkspaceResolved initialized the match slice before the only branch
that consumes it and then replaced that value immediately.
Fix:
- Declare the match result at the call site and return its dedicated error.
Verification:
- GOOS=windows golangci-lint run --timeout=5m ./... in desktop.
- go test ./internal/workspacestate in desktop.
- go run ./tools/repolint.
Problem: Stop could stay disabled after a failed cancellation, target another tab after navigation, or submit duplicate requests from question cards.
Root cause: the local view discarded the cancellation promise, the controller erased failure receipts, approval Stop deferred source capture, and question Stop had no independent pending lock.
Fix: share the prompt Stop lifecycle, capture the source synchronously, expose local cancellation failures to decision cards, propagate remote failures, and clear question drafts only after accepted cancellation.
Verification: production build and bundle gates, test typecheck, 27 approval assertions, 36 controller assertions, session command and question draft regressions, 115 remote assertions, and browser Stop failure/retry checks passed.
Problem:
- Windows path identity changes could render one physical workspace twice and
make both sidebar rows share expansion state.
- A blocked session open could hold the runtime rebuild lock and prevent a
tray-initiated shutdown from completing.
Root cause:
- Desktop projections re-derived workspace IDs from path spelling instead of
resolving the persisted physical owner.
- Session navigation cancellation was not admitted atomically with shutdown,
and request cleanup could also cancel an already published controller.
Fix:
- Resolve workspace ownership by physical identity at the persistence boundary
and revalidate discovery under the cross-process writer lock.
- Cancel pending navigation before shutdown waits for runtime locks, reject
stale or late requests, and separate request cancellation from the published
controller lifetime.
- Record precise shutdown lock-wait phases and add deterministic regressions
for identity, navigation, and shutdown ordering.
Verification:
- Focused Desktop regression tests with the race detector.
- go vet ./... in desktop.
- go run ./tools/repolint.
- Windows amd64 Desktop test binary cross-compilation.
Problem:
CodeQL reports that adding two attacker-influenced slice lengths can overflow before allocating scheduler claim state.
Root cause:
mergeRealized used len(existing)+len(add.Paths) directly as map and slice capacity.
Fix:
Use the larger input length as the allocation hint. Appends retain identical deduplication behavior without overflow-prone arithmetic.
Verification:
Focused scheduler and write-claim tests, agent go vet, and golangci-lint v2.12.2 pass.
Problem:
The Windows isolated agent suite looked up bash after ConfineBash correctly registered the executor as pwsh.
Root cause:
The cross-platform write-root test hard-coded the POSIX tool name instead of using the confined shell identity.
Fix:
Derive lookup and removal assertions from shell.Name so the same sandbox invariant is tested on POSIX and Windows.
Verification:
The focused test, agent go vet, and a Windows amd64 agent test-binary build pass.
Problem:
Linux golangci-lint treated the Windows-only runner report environment constant as unused.
Root cause:
The declaration lived in a cross-platform file while every reference was behind the Windows build tag.
Fix:
Move the constant into the Windows diagnostics implementation so each platform compiles only the declarations it owns.
Verification:
golangci-lint v2.12.2, repolint, sandbox tests, winsandbox tests, and go vet pass.
Problem:
The CI lint job rejected a fixed-count checkpoint regression loop under the Go 1.22 intrange rule.
Root cause:
The test used the pre-Go-1.22 three-clause form for a constant integer range.
Fix:
Use the native integer range syntax.
Verification:
- go test ./internal/transcript -run 'Test.*Checkpoint' -count=1
- golangci-lint run --timeout=5m
- git diff --check
Problem:
Windows workspace-write sessions could stall in nested shell probes, expose mixed Bash and PowerShell contracts, and replace repeated tool results with placeholders. Background services such as OpenMAIC could not start reliably.
Root cause:
The Windows path reused the persistent Bash lifecycle, ran a second PowerShell launch check before every command, modeled background work outside the job owner, and deduplicated results across distinct call ids.
Fix:
Expose a stable pwsh contract on Windows, run one-shot commands directly through the restricted-token runner, classify signed runner failures, route services through job_output/job_kill, preserve one result per call, and keep legacy aliases behind the same sandbox. Simplify Settings to platform-relevant runtimes and move diagnostics into an expandable section.
Verification:
Focused agent, boot, sandbox, shellrun, tool, winsandbox, desktop, frontend typecheck, CSS, and shell settings contract tests passed on macOS. Native Windows coverage is delegated to the PR CI matrix.
Problem:
App memory screening could not open tool-heavy benchmark sessions after snapshot validation became strict.
Root cause:
The frontend treated a toolCallId-derived identity as a mandatory match even when canonical history legitimately addressed the same tool result by messageId.
Fix:
Keep explicit snapshot identities authoritative, use derived identities only as compatibility fallbacks, and retain messageId as the canonical history merge key. Add a follower regression for tool rows carrying both identifiers.
Verification:
- pnpm test:transcript
- pnpm exec tsc --noEmit -p tsconfig.test.json
- pnpm build
- REASONIX_APP_MEMORY_PROFILE=short node bench/app-memory.mjs
- go run ./tools/repolint
Problem:
Draft Git controls could target the preceding formal session, while an empty
backing tab could hide draft recovery actions. The runtime file also exceeded
the repository's existing size budget.
Root cause:
Draft presentation inherited formal-tab identity and combined the hidden tab's
hero state with the active draft's layout. Shell presentation logic had been
added to the already large runtime lifecycle owner.
Fix:
Project draft workspace labels in the composer builder and clear inherited
tab IDs and Git branches. Give active drafts sole ownership of hero visibility
and extract presentation predicates into the shell layer.
Verification:
- focused workspace-context and draft-presentation regressions
- pnpm test:typecheck
- pnpm build
- go run ./tools/repolint
- browser check for the clean project draft and project selector
Problem: write-access confirmation could leave a prompt disabled indefinitely, and initial Desktop sessions could not submit exact approvals.
Root cause: session generation zero was treated as missing across the frontend and host boundary, while cancelled or rejected prompt operations lost their outcomes before reaching the card.
Fix: accept explicit initial generations while preserving exact identity fencing, propagate command and transport outcomes, keep Stop independent, reset state by full prompt identity, and add bounded diagnostic events.
Verification: frontend production/test typechecks, production build and budgets, focused prompt/UI/remote/export tests, Desktop exact-prompt race tests, controller consecutive-write authorization tests, and browser interaction checks.
Problem:
New-session drafts opened a full draft management page and looked like failed session creation.
Root cause:
The durable draft lifecycle was presented as a separate management surface, so the UI exposed persistence details instead of the ordinary new-session experience. The composer workspace also inherited the backing tab instead of the draft owner.
Fix:
Render healthy drafts on the standard welcome composer, keep recovery states explicit, move draft controls into the topic bar, and derive workspace context from the draft owner.
Verification:
- pnpm build
- pnpm test:app-lifecycle
- focused draft presentation and workspace context tests
- browser interaction checks for draft restore and new-session entry points
Co-authored-by: F <[email protected]>
Problem:
Interrupted and outside-turn display frames could reach transcript snapshots without a record identity. Desktop then rejected the snapshot during hydration, and persisted checkpoints could reproduce the failure on every reopen.
Root cause:
The projection only derived identities from tool calls, message IDs, or positive business sequences. Zero-sequence display frames had no fallback identity, snapshot publication did not validate uniqueness, and Desktop mutated resident transcript state before all validation completed.
Fix:
Assign projection-scoped identities at the transcript owner, validate every frozen snapshot, repair legacy checkpoint rows, and normalize compatible old-peer snapshots. Stage Desktop store replacement until the reducer accepts the cut while preserving in-flight content ownership.
Verification:
- go test ./... -count=1
- go vet ./internal/transcript ./internal/control
- go run ./tools/repolint
- pnpm test:transcript
- pnpm build
- TypeScript production and test typechecks
Problem: Windows CI rejected an upper-case spelling of a valid transcript path,
so one file could produce an empty runtime key instead of sharing its lease key.
The invalid-route regression also expected an absent-path error for a spelling
that Windows rejects before filesystem lookup.
Root cause: the new Desktop transcript validator applied the store's
case-sensitive filename check before Windows path identity folding.
Fix: fold transcript names only on Windows before applying the existing file
type exclusions, and accept the native invalid-name result when asserting that
malformed route artifacts were not created.
Verification: focused tests and race tests passed; Desktop go vet, repolint,
git diff --check, and Windows amd64 test compilation passed.
Problem: blocked startup records could still write pinned-context sidecars,
exact routes ignored conflicting pending operations, and canonical tabs with
conflicting stored locators could pass hidden-tab persistence checks.
Root cause: sidecar restoration ran outside the restore-block branch, pending
identity resolution omitted registry operations, and metadata validation only
examined an optional requested path instead of the stored tab identity.
Fix: preserve blocked goal and pin fields without file access, validate pin
migration targets, include operation targets and mappings in identity checks,
and reject conflicting stored identities before skipping legacy metadata.
Verification: focused restore, reconciliation, metadata, and pruning tests;
focused race tests; Desktop go vet; repolint and git diff --check. Regression
coverage includes unchanged registry state and preservation of original data.
Problem:
Windows /clear failed while removing session.inbox/transaction.lock because the live Controller still held the lock file open.
Root cause:
ClearSession deleted old session artifacts before rebinding and closing the old inbox Store. Unix permits unlinking an open file, which hid the ordering bug, while Windows rejects it with a sharing violation.
Fix:
Join active inbox scans, close and detach the old inbox Store before destructive cleanup, and rebind the current inbox if cleanup or fresh-session binding fails.
Verification:
- go test ./internal/control -run '^(TestSubmitClearDiscardsCurrentContextWithoutSavingTranscript|TestClearSession|TestSessionEventsFireAtLifecyclePoints)' -count=20 -timeout=10m
- go test ./internal/control ./internal/sessioninbox -timeout=15m
- golangci-lint run --timeout=5m
Problem:
Windows /clear failed while removing session.inbox/transaction.lock because the live Controller still held the lock file open.
Root cause:
ClearSession deleted old session artifacts before rebinding and closing the old inbox Store. Unix permits unlinking an open file, which hid the ordering bug, while Windows rejects it with a sharing violation.
Fix:
Join active inbox scans, close and detach the old inbox Store before destructive cleanup, and rebind the current inbox if cleanup or fresh-session binding fails.
Verification:
- go test ./internal/control -run '^(TestSubmitClearDiscardsCurrentContextWithoutSavingTranscript|TestClearSession|TestSessionEventsFireAtLifecyclePoints)' -count=20 -timeout=10m
- go test ./internal/control ./internal/sessioninbox -timeout=15m
- golangci-lint run --timeout=5m
Problem:
Windows /clear failed while removing session.inbox/transaction.lock because the live Controller still held the lock file open.
Root cause:
ClearSession deleted old session artifacts before rebinding and closing the old inbox Store. Unix permits unlinking an open file, which hid the ordering bug, while Windows rejects it with a sharing violation.
Fix:
Join active inbox scans, close and detach the old inbox Store before destructive cleanup, and rebind the current inbox if cleanup or fresh-session binding fails.
Verification:
- go test ./internal/control -run '^(TestSubmitClearDiscardsCurrentContextWithoutSavingTranscript|TestClearSession|TestSessionEventsFireAtLifecyclePoints)' -count=20 -timeout=10m
- go test ./internal/control ./internal/sessioninbox -timeout=15m
- golangci-lint run --timeout=5m
Problem:
Windows /clear failed while removing session.inbox/transaction.lock because the live Controller still held the lock file open.
Root cause:
ClearSession deleted old session artifacts before rebinding and closing the old inbox Store. Unix permits unlinking an open file, which hid the ordering bug, while Windows rejects it with a sharing violation.
Fix:
Join active inbox scans, close and detach the old inbox Store before destructive cleanup, and rebind the current inbox if cleanup or fresh-session binding fails.
Verification:
- go test ./internal/control -run '^(TestSubmitClearDiscardsCurrentContextWithoutSavingTranscript|TestClearSession|TestSessionEventsFireAtLifecyclePoints)' -count=20 -timeout=10m
- go test ./internal/control ./internal/sessioninbox -timeout=15m
- golangci-lint run --timeout=5m
Capture a classified clear failure immediately and include all goroutine stacks when the asynchronous /clear completion exceeds its deadline. This keeps the regression actionable on Windows instead of reporting only a silent timeout.
Capture a classified clear failure immediately and include all goroutine stacks when the asynchronous /clear completion exceeds its deadline. This keeps the regression actionable on Windows instead of reporting only a silent timeout.
Capture a classified clear failure immediately and include all goroutine stacks when the asynchronous /clear completion exceeds its deadline. This keeps the regression actionable on Windows instead of reporting only a silent timeout.
Capture a classified clear failure immediately and include all goroutine stacks when the asynchronous /clear completion exceeds its deadline. This keeps the regression actionable on Windows instead of reporting only a silent timeout.
Problem:
Windows control CI repeatedly timed out waiting for /clear after the attachment admission refactor.
Root cause:
Known session-management commands entered the model-turn attachment preparation and admission lifecycle even though they never admit a turn and may rotate the attachment owner asynchronously.
Fix:
Route /new, /clear, /context, and /compact through the original submission gate while keeping ordinary prompts, references, and attachments on the unified admission path.
Verification:
- go test ./internal/control -run '^(TestSubmitClearDiscardsCurrentContextWithoutSavingTranscript|TestClearSession|TestNewSession)' -count=20 -timeout=10m
- go test ./internal/control -timeout=15m
- golangci-lint run --timeout=5m
Problem:
Windows control CI repeatedly timed out waiting for /clear after the attachment admission refactor.
Root cause:
Known session-management commands entered the model-turn attachment preparation and admission lifecycle even though they never admit a turn and may rotate the attachment owner asynchronously.
Fix:
Route /new, /clear, /context, and /compact through the original submission gate while keeping ordinary prompts, references, and attachments on the unified admission path.
Verification:
- go test ./internal/control -run '^(TestSubmitClearDiscardsCurrentContextWithoutSavingTranscript|TestClearSession|TestNewSession)' -count=20 -timeout=10m
- go test ./internal/control -timeout=15m
- golangci-lint run --timeout=5m
Problem:
Windows control CI repeatedly timed out waiting for /clear after the attachment admission refactor.
Root cause:
Known session-management commands entered the model-turn attachment preparation and admission lifecycle even though they never admit a turn and may rotate the attachment owner asynchronously.
Fix:
Route /new, /clear, /context, and /compact through the original submission gate while keeping ordinary prompts, references, and attachments on the unified admission path.
Verification:
- go test ./internal/control -run '^(TestSubmitClearDiscardsCurrentContextWithoutSavingTranscript|TestClearSession|TestNewSession)' -count=20 -timeout=10m
- go test ./internal/control -timeout=15m
- golangci-lint run --timeout=5m
Problem:
Windows control CI repeatedly timed out waiting for /clear after the attachment admission refactor.
Root cause:
Known session-management commands entered the model-turn attachment preparation and admission lifecycle even though they never admit a turn and may rotate the attachment owner asynchronously.
Fix:
Route /new, /clear, /context, and /compact through the original submission gate while keeping ordinary prompts, references, and attachments on the unified admission path.
Verification:
- go test ./internal/control -run '^(TestSubmitClearDiscardsCurrentContextWithoutSavingTranscript|TestClearSession|TestNewSession)' -count=20 -timeout=10m
- go test ./internal/control -timeout=15m
- golangci-lint run --timeout=5m
Problem:
The history/export PR carries the alpha helper first consumed by the performance layer, so standalone lint reports it as unused.
Root cause:
The helper is intentionally staged before the bounded variant encoder commit.
Fix:
Add a narrow, documented unused suppression until the final layer activates it.
Verification:
- golangci-lint run --timeout=5m
Problem:
The reliability PR still carries helpers first consumed by later stack layers, so standalone lint reports them as unused.
Root cause:
Those helpers are intentionally staged before the history and performance commits that activate them.
Fix:
Add narrow, documented unused suppressions on the staged helpers.
Verification:
- golangci-lint run --timeout=5m
Problem:
The first attachment PR defined helpers consumed only by later stack layers, so standalone golangci-lint rejected the branch.
Root cause:
The stack was tested at the final head, while the intermediate branch exposed staged unexported helpers to the unused analyzer.
Fix:
Document and narrowly suppress unused analysis for helpers until their owning downstream layer activates them.
Verification:
- go test ./internal/attachment ./internal/control ./internal/session
- golangci-lint run --timeout=5m
Problem: Repeated image preparation duplicated bounded transforms, and content
publication and legacy path reads needed stronger cross-platform path safety.
Root cause: Variants were computed per request without a deterministic policy
key, while no-replace publication and file identity checks were spread across
platform-specific call sites.
Fix: Add deterministic variant caching with cancellation-safe authorization,
centralize no-replace writes in fileutil, harden legacy path validation, and add
platform CI inventory plus packaged attachment smoke documentation.
Verification: Focused attachment, control, fileutil, and view-image tests passed
with and without the race detector. macOS group and CI workflow contract tests,
repolint, Desktop inventory, and git diff checks passed on latest main-v2.
Problem: Accepted images were durable for execution but historical messages and
session exports could not reliably recover the authorized image bytes.
Root cause: History projection omitted attachment references and export copied
event payload content without collecting the image refs embedded in those
payloads.
Fix: Project digest-backed attachment cards into history, add bounded session
attachment reads, collect authorized image refs while exporting, and render the
same closure in exported session documents.
Verification: Session and session-export suites, Desktop attachment tests,
frontend history and draft-read tests, frontend typecheck, repolint, and git
diff checks passed on the latest main-v2 path-identity implementation.
Problem: Queued image submissions could outlive their temporary credentials or
source files, and retries could create a second logical turn.
Root cause: The inbox persisted text and transient references without one
durable fingerprint for the accepted image batch and submission identity.
Fix: Persist compatible image fingerprints and content snapshots in inbox
schema 3, preserve edit failure atomicity, and make queued replay reuse the
original admitted submission fact.
Verification: Focused session-inbox and control attachment, schema, submission,
and queue tests passed with and without the race detector. Repolint and git diff
checks passed.
Problem: Composer and CLI image references could lose their bytes before the
provider request, leaving path text to fall through to tools and Bash.
Root cause: Image preparation was split across focus-sensitive Desktop RPCs,
temporary draft credentials, controller state, and request-time model routing
instead of one durable session admission boundary.
Fix: Admit ordered image batches before creating the user turn, freeze accepted
sources into session-owned image inputs, bind Desktop targets to session
identity, and preserve legacy provider serialization and CLI path behavior.
Verification: Focused attachment, control, provider, boot, serve, tool, Desktop,
and frontend suites passed on the latest main-v2 base. Frontend typecheck,
repolint, and git diff checks also passed.
Problem:
The path identity branch conflicted with the protocol v11 diagnostics lifecycle merged into main-v2, leaving its terminal checks unable to run.
Root cause:
Both changes extended the Desktop hello result and startup logging from the older protocol baseline.
Fix:
Keep protocol v11 run and incident diagnostics plus the ShellLifecycle flow, layer the optional identity summary into the same hello result, document both fields in English and Chinese, and add a symlink escape regression at the Serve session boundary.
Verification:
- go test focused identity, lock, agent, repair, serve, and catalog packages
- go test -race for pathidentity, filelock, identitylock, and workspacelease
- Desktop hostrpc, instanceidentity, and workspacestate tests
- Electron typecheck and 224 tests
- repolint, workflow tests, and git diff --check
Problem: concurrent identity lock initialization could reject a lock file created between filesystem observations, while symlink replacement and Windows-only diagnostics still had coverage and lint gaps.\n\nRoot cause: path resolution treated an observation race as a dangling link, repair validation compared only native file identity, and Windows tests exercised invalid relative fixtures.\n\nFix: check entry existence before resolving links, retain symlink destinations during repair validation, use errors.As for NTStatus, and make Windows fixtures use absolute temporary roots. Add deterministic regressions for the race and inode-reuse redirect.\n\nVerification: go test ./..., desktop go test -timeout 20m ./..., focused race tests, cross-platform golangci-lint, repolint, and Windows workspacestate cross-compilation.
Problem
A fast HTTP turn could run to completion before Serve sampled Controller.Running, causing the accepted request to receive 409. This failed TestServeSubmitRunsAndBroadcastsTurnDone on Ubuntu after the runner had already executed.
Root cause
Legacy requests without a submission ID inferred admission from transient runtime state. That state cannot distinguish an immediately completed turn from a rejected turn.
Fix
Route legacy Serve turns through the controller's synchronous submission boundary and use its monotonic turn token to report whether the request was admitted. Keep rejected submissions as explicit ErrSubmissionNotAccepted results and route the typed final-readiness compatibility action through the same boundary.
Verification
- go test ./...
- go test ./internal/control ./internal/serve
- go test ./internal/serve -run '^TestServeSubmitRunsAndBroadcastsTurnDone$' -count=500
- go test ./internal/control ./internal/serve -run 'TestLegacySubmissionReportsSynchronousAdmission|TestServeSubmitRunsAndBroadcastsTurnDone' -count=50
- go test -race ./internal/control ./internal/serve -run 'TestLegacySubmissionReportsSynchronousAdmission|TestServeSubmitRunsAndBroadcastsTurnDone' -count=5
- go run ./tools/repolint
- git diff --check
Problem
Windows Desktop CI could show a recovered transcript as a second ordinary topic, while the worktree matrix could be killed by its outer step timeout before the Go timeout reported a result.
Root cause
Legacy topic migration and catalog recovery folding projected the same recovery file independently, so execution order changed the catalog. The worktree suite also normally approaches the workflow's ten-minute ceiling even though its own Go timeout remains eight minutes.
Fix
Skip recovery transcripts as standalone legacy topics while their parent transcript exists, while still promoting them after parent removal. Give the isolated Windows step enough reporting headroom and lock that budget in its workflow test.
Verification
- cd desktop && go test ./...
- cd desktop && go test ./... -run 'TestLegacyRecoveryMigrationWaitsForParentRemoval|TestAmbiguousLegacyRecoverySessionsMigrateIntoTopics' -count=10
- cd desktop && go test -race ./... -run 'TestLegacyRecoveryMigrationWaitsForParentRemoval|TestAmbiguousLegacyRecoverySessionsMigrateIntoTopics' -count=3
- node --test scripts/windows-go-tests.test.mjs scripts/ci-workflow.test.mjs
- go run ./tools/repolint
- go run github.com/rhysd/actionlint/cmd/[email protected] .github/workflows/ci.yml
- git diff --check
Problem:
Windows packaging reused the verified frontend artifact but failed because the artifact omitted the commit-scoped sourcemap archive required by package.mjs.
Root cause:
Desktop preparation uploaded only dist and the artifact identity manifest even though the frontend build also produces frontend/sourcemaps/<commit>.
Fix:
Upload the exact GitHub SHA sourcemap directory with both stable and canary frontend artifacts, and lock the two-variant contract in the CI workflow test.
Verification:
- ci-workflow tests: 22 passed
- actionlint: passed
- repolint: clean
- git diff --check: passed
Problem:
Desktop CI failed Go 1.22 copyloopvar lint and a migration test still expected equivalent failures from separate startups to collapse into one upload.
Root cause:
The shutdown loop retained a pre-Go-1.22 closure copy, while the migration assertion encoded the retired DedupKey transport-dedup behavior.
Fix:
Remove the redundant loop copy and assert that separate startup failures keep distinct EventIDs while sharing the same correlation key.
Verification:
- desktop full test suite passed
- I-P race partition passed
- targeted migration test passed 10 runs
- targeted migration race test passed 3 runs
- golangci-lint and repolint passed
- git diff --check and gofmt passed
Problem:
The diagnostics lifecycle change exceeded the repository's existing-file line budgets in the frontend crash reporter and Worker entrypoint, causing lint-code to fail.
Root cause:
New report attribution, regression, and crash-family logic was added directly to already-large owner modules.
Fix:
Extract Worker report classification and version/regression helpers into a focused module, update tests to import their owner, and compact the small frontend crash-family addition without changing behavior.
Verification:
- frontend: 407 suites passed; typecheck passed
- crash-report worker: 137 tests passed; typecheck passed
- repolint: clean
- git diff --check: passed
Preserve the new catalog initial-reconcile ownership gate alongside the staged shutdown coordinator. Regenerate the Desktop v11 contract and command ownership against the combined App API.
Verification:
- Focused Desktop Go lifecycle and host RPC tests
- Electron 224 tests and typecheck
- Crash worker 137 tests and typecheck
- Frontend typecheck
Problem:
Desktop shutdown, connection loss, and startup failure followed separate cleanup paths. Save failures could be followed by service termination, while crash reports mixed observer and failing-process identities and could reopen resolved historical groups.
Root cause:
Shutdown ownership and progress were duplicated across Go and Electron, lifecycle evidence lacked stable run/event identities, and worker projection used transport metadata as fault attribution.
Fix:
Introduce a retryable staged shutdown coordinator and shell status protocol, preserve lifecycle evidence through EOF and signals, add diagnostics v4 event/incident attribution and idempotent projection, separate historical/regression decisions, and validate release sourcemaps plus the Top 10 governance dry-run.
Verification:
- Desktop Go full package tests and focused race tests
- Electron 224 tests and typecheck
- Frontend 406 suites and typecheck
- Crash worker 137 tests and typecheck
- macOS arm64 package, sourcemap manifest, and offline symbolization
Problem:
The final PR lint job rejected a nil context in the link-redirection regression and a lease publication helper left unused after compatible lock ownership moved onto SessionLease.
Root cause:
The regression used Acquire nil-context compatibility directly, while the lock refactor replaced the standalone publication path without deleting its superseded helper.
Fix:
Pass an explicit background context in the regression and remove the dead standalone lease publication function.
Verification:
- golangci-lint run --timeout=5m
- go run ./tools/repolint
- go test ./internal/identitylock ./internal/agent ./internal/serve ./internal/workspacelease
- cd desktop && go test -timeout 20m . ./internal/workspacestate ./internal/instanceidentity ./internal/hostrpc
- cd desktop && go test -run ^$ .
Problem:
An identity-aware lock could resolve an alias, then open a different file if the final link was redirected between resolution and acquisition.
Root cause:
The composition layer passed the original identity key into the process-local queue but did not verify that the access path still resolved to that key after the OS lock was acquired.
Fix:
Re-resolve every ordinary identity-aware lock after acquisition, release it on resolution failure or identity change, and return a closed error. Add a deterministic symlink-redirection regression that also proves the rejected acquisition releases its lock.
Verification:
- go run ./tools/repolint
- go test ./internal/identitylock ./internal/filelock ./internal/pathidentity
- go test -race ./internal/identitylock ./internal/filelock
- Windows amd64 cross-compile for identitylock, pathidentity, and filelock
Problem:
Windows native tests failed every identity-aware file lock with ERROR_NOACCESS, while the first path-identity integration also exceeded repository size, complexity, and layering limits.
Root cause:
The Windows implementation queried FileCaseSensitiveInfo through GetFileInformationByHandleEx, which is rejected on the CI runner. The utility-layer filelock package also imported the higher-level pathidentity package directly, and several identity workflows remained embedded in oversized owners.
Fix:
Query per-directory case sensitivity with NtQueryInformationFile, preserving explicit unsupported-information fallbacks. Add an identitylock composition layer that resolves access paths and passes comparison keys into filelock's explicit local-queue API, then migrate production file locks to it. Keep custom session, repair, and workspace lock protocols on explicit keys, and extract path and acquisition logic into focused owner files.
Verification:
- go run ./tools/repolint
- go test -run '^$' ./...
- go test -race ./internal/filelock ./internal/identitylock ./internal/workspacelease
- Windows amd64 cross-compile for affected root packages
Problem
The PR conflicted with new saved-tab reconciliation and release-pipeline changes merged into main-v2.
Root cause
Both branches regenerated the desktop migration inventory, while the saved-tab work also changed the tab persistence owners touched by session interaction isolation.
Fix
Merge current main-v2, retain its persistence reconciliation and unknown-field preservation alongside generation-aware interaction state, and regenerate the 860-entry desktop inventory from the combined code.
Verification
- go run ./tools/desktopinventory -check
- go test . -run 'Test(MetaForTab|RuntimeState|SavedTab|TabPersistence|Restore|RemoteTab)' -count=1 (desktop)
- go test ./internal/control ./internal/event ./internal/eventwire ./internal/extension/uihub ./internal/serve
- pnpm exec tsx src/__tests__/use-controller-send-fallback.test.tsx (29/29)
- pnpm typecheck
- pnpm test:typecheck
- go run ./tools/repolint
- git diff --check
Problem: Windows push CI failed TestSessionDeleteWithStuckJobReturnsAfterSingleGrace when a loaded runner exceeded a wall-clock allowance by 11.7 ms.
Root cause: The test inferred that session deletion waited only once from elapsed time, so scheduler and filesystem load could fail a correct teardown path.
Fix: Record teardown timeout events, use a zero-duration grace, and assert exactly one destroy-session wait with no close wait. Move the teardown fixture into a focused test file to keep the existing server test within its lint budget.
Verification: focused regression 100 iterations; race regression 20 iterations; full internal/acp tests; go vet; repolint; Windows amd64 test compilation.
Problem
The full desktop frontend CI suite still modeled Ask fallback submission through the retired tab-scoped endpoint and failed once production required exact session submission.
Root cause
The fixture omitted session generation, runtime epoch, and pending-prompt identity lookup, so its successful completion could not match the card's immutable interaction target.
Fix
Model the current session-bound prompt contract, resolve missing turn identity only through PendingPromptIdentitiesForTab, and assert that submission never borrows the latest tab turn.
Verification
- REASONIX_TEST_CONCURRENCY=2 node frontend/scripts/run-ci-tests.mjs (459/459)
- pnpm exec tsx src/__tests__/use-controller-send-fallback.test.tsx (29/29)
- pnpm typecheck
- pnpm test:typecheck
- go run ./tools/repolint
- git diff --check
Problem: main-v2 merged #10502 with the same credential path-alias repair, creating conflicts in the production code, native test, and CI contract.
Resolution: adopt main-v2's production implementation and retain this branch's isolated TMP/TEMP fixture plus explicit config and winsandbox PR coverage.
Verification: Windows cross-compile and vet for config/winsandbox; CI workflow contract tests; actionlint; repolint; diff checks.
Problem: main-v2 advanced with the unrelated saved-tab reconciliation merge while the Windows credential fix was being prepared.
Fix: merge the latest protected branch so the pull request candidate is tested against the current integration baseline.
Verification: the base delta only touches Desktop session persistence files and does not overlap the ACL or CI changes.
Problem: main-v2 Windows full CI rejected stale credential markers when the credential path resolved from an 8.3 alias to its long spelling.
Root cause: legacy ACL recovery compared marker and repair paths as strings even though both names referred to the same protected file.
Fix: expand Windows long paths before matching, cover a real 8.3 alias, and run the native credential ACL regressions on pull requests.
Verification: Windows cross-compile and vet for config/winsandbox; CI workflow contract tests; actionlint; repolint; diff check.
* Fix saved tab reconciliation during desktop startup
Problem: stale saved tabs after session migration could repeatedly fail startup, recreate fallback sessions, or hide recovery-owned state.
Root cause: tab restoration trusted missing canonical identities before migration and lifecycle reconciliation converged, while startup writes were not fenced against newer tab mutations.
Fix: reconcile persisted tabs against canonical, draft, recovery, migration, and durable-artifact ownership before publication; conditionally archive only proven-empty sessions; preserve unknown JSON fields; and fence repaired snapshots with monotonic versions.
Verification: focused desktop reconciliation and cleanup tests, focused go test -race, go vet ./..., Desktop A-C and D-F test shards, and all Desktop internal/cmd packages pass on the latest main-v2.
* Refine saved tab recovery ownership
Problem: the saved-tab recovery change exceeded repository lint budgets, and its pending-operation guard also treated committed migration receipts as active owners.
Root cause: startup reconciliation and tab persistence remained embedded in oversized owner files, while legacy cleanup did not distinguish unfinished operations from durable committed receipts or its own archive operation.
Fix: extract startup reconciliation, persistence I/O, workspace validation, and persistent-owner checks into focused owners; protect only unfinished external operations while preserving recovery ownership.
Verification: repolint, git diff --check, Desktop go vet, focused saved-tab and legacy-cleanup tests, focused race tests, Desktop internal/cmd tests, and the isolated v3 upgrade test pass.
* Fix Windows-tagged reconciliation lint
Problem: the cross-platform lint leg rejected two reconciliation assignments under Windows build tags.
Root cause: one returned snapshot version was intentionally discarded after the restore fence, while migration evidence was initialized before a branch that always replaced it.
Fix: discard the unused return explicitly and scope post-migration evidence to the branch that consumes it.
Verification: GOOS=windows golangci-lint run --timeout=5m ./..., Windows go vet, repolint, git diff --check, and focused Desktop tests pass.
* Make catalog migration tests event-driven
Problem: the Windows I-P Desktop shard could fail while waiting for a migrated CLI session to appear, even though the catalog scan was still making progress.
Root cause: catalog topic tests approximated completion with a five-second polling deadline, and each ListProjectTree call could itself block long enough to overrun that deadline on a loaded Windows runner.
Fix: publish a generation-scoped completion signal for the initial catalog reconciliation, wait on existing explicit reconcile job completion for later mutations, and assert the tree only after the authoritative work has finished. Add a deterministic restored-tabs barrier regression.
Verification: repolint, git diff --check, Desktop and Windows go vet, Windows golangci-lint, repeated focused and race tests, CI partition contract tests, and the complete local I-P shard pass.
Problem:
The packaged navigation checks targeted retired workspace-browser markup and pre-draft blank-session behavior.
Root cause:
The qualification scripts had not followed the ProjectTree and persistent draft lifecycle, so they failed before exercising the product invariants.
Fix:
Drive the current sidebar and ProjectTree, assert blank drafts leave no canonical residue, and retain exact SessionID, transcript, rapid-switch, archive, restore, and restart checks.
Verification:
Packaged navigation smoke
Independent native session smoke with fork, background completion, archive, restore, and two restarts
Problem:
Updater and ordinary startup could derive different instance identities, while the host handshake exposed only an opaque legacy hash.
Root cause:
The comparison key was reused as an access path and Shell and service did not exchange a versioned filesystem identity summary.
Fix:
Keep REASONIX_HOME as an absolute access path, derive instance identity separately, validate homes through pathidentity, and add optional identity version, digest, and legacy ID fields to the compatible hello contract.
Verification:
Electron typecheck and 219 tests
Generated desktop contract freshness check
Packaged shell/service handshake and clean-exit smoke
Problem:
Replacing legacy path keys directly would let new and v1.38.10 processes write the same physical workspace or session under different lock names.
Root cause:
File I/O paths, process-local queue keys, and historical lock identities were coupled, and multi-lock acquisition lacked one ordered compatibility protocol.
Fix:
Acquire deduplicated current and frozen legacy locks in deterministic order, release partial holds on contention, revalidate identity before writes, and retain directory-entry semantics for repair and hierarchy operations.
Verification:
go test -race -count=1 ./internal/filelock ./internal/workspacelease ./internal/agent ./internal/repair
Bidirectional legacy/current lease contention and link-redirection regressions
Windows and Linux amd64 cross-compilation
Problem:
Equivalent directory spellings could register separate workspaces or fail session creation and recovery, including Global startup and macOS case aliases.
Root cause:
Registration, binding, resume, reattach, and Serve ownership checks used different string-based path rules outside one cross-process decision boundary.
Fix:
Resolve physical identity inside the registry transaction, preserve existing IDs and unknown fields, reject ambiguous legacy owners, and route session ownership and Global bootstrap through the shared resolver.
Verification:
cd desktop && go test -timeout 20m .
go test ./internal/serve
go test -race -count=1 ./internal/serve
Packaged Global create, restart, and A/B/A navigation smokes
Problem:
Workspace and session catalogs implemented platform path comparison independently, so aliases could diverge across case, link, and missing-path forms.
Root cause:
There was no shared representation separating an access path, resolved physical path, and comparison key with explicit link semantics.
Fix:
Add the versioned pathidentity resolver, move catalog identity handling onto it, rebuild the derived identity projection at schema 13, and wire the platform test selectors.
Verification:
go test ./...
go test -race -count=1 ./internal/pathidentity ./internal/sessioncatalog
Windows and Linux amd64 cross-compilation for the affected packages
Problem
The full frontend CI suite crashed because a stale-prompt regression still dispatched the retired id-only submit failure action.
Root cause
The implementation now requires submit failures to carry the immutable interaction target, but this test fixture had not adopted the new action contract.
Fix
Give the fixture a generation-aware session owner and use exact old/new interaction targets for optimistic clears and failure callbacks.
Verification
pnpm exec tsx src/__tests__/pending-prompt-stale-status.test.tsx
pnpm typecheck
pnpm test:typecheck
go run ./tools/repolint
git diff --check
Problem: native Windows CI still rejected short/long aliases after the first ACL fix, while an SSH cancellation check failed on runner timing.
Root cause: file identity lookup reopened a file after its deny ACL was active; the SSH fixture could fail on an invalid pipe remote address before installing its watcher and asserted a 40 ms scheduler budget.
Fix: compare exact or Win32-expanded long paths without reopening protected files, exercise a real 8.3 alias, and synchronize SSH cancellation on the watcher deadline event with a valid remote address.
Verification: SSH regression 100 iterations; Windows cross-compile and vet for winsandbox, config, and remote; repolint and diff check.
Problem
The approval animation browser gate timed out because the preview card remained mounted after confirmation.
Root cause
Browser mock metadata omitted session generation and prompt events omitted turn/runtime identity, so the exact interaction path could not own or resolve the rendered request.
Fix
Give established mock sessions stable identities, attach per-tab turn identity to approval, Ask, recovery, plan, and MCP prompts, and add actionable diagnostics to the browser gate. Keep blank draft tabs without a session identity and extract the state owner so the repository lint baseline does not grow.
Verification
pnpm typecheck
pnpm test:typecheck
pnpm test:motion
pnpm build
pnpm test:motion-browser
go run ./tools/repolint
git diff --check
Problem:
A remote session could render an empty transcript (0 turns) while the serve
served the whole conversation: the early history read replaced the transcript
after the follower had published the authoritative live cut.
Root cause:
primeEarlyHistory dispatched history_replace with whatever the window read
returned. history_replace has no revision guard in the reducer, and the prime
only checked whether hydration had finished: a stale or empty window resolving
between the follower's first cut and hydration's completion wiped the
conversation. The local prime fingerprints its read against the tab meta; the
remote one had no equivalent guard.
Fix:
Only a blank transcript may be primed: skip the replacement when the mounted
transcript already holds items or a live tail, so the live cut always wins.
Verification:
- desktop/frontend: npx tsc --noEmit, npm run build (bundle budgets pass)
- manual: the AionCore remote tab renders its transcript (1 turn) again instead
of the empty hero
Problem: main-v2 Windows full CI rejects a valid stale credential marker when a short user path is resolved to its long form before ACL repair.
Root cause: the marker guard compared path spelling even though the repair owner acts on the same underlying file; this also made the native config regression absent from PR CI.
Fix: compare os.Stat file identities, retain the exact ACL and exited-owner checks, add a hardlink-alias regression, and run focused config ACL tests on Windows PRs.
Verification: Windows cross-compile and vet for winsandbox/config, CI workflow contract tests, actionlint, repolint, and inventory check. Native execution remains the new PR CI gate.
Problem:
With a draft (new session) surface visible while a remote tab was the active
tab, the composer's workspace chip showed the remote workspace (for example
AionCore) even though the workspace picker lists only local workspaces.
Root cause:
AppRuntimeView derived the composer workspace context from the raw active tab,
remote tabs included, while the draft surface only clears the topicbar's active
tab: the shared composer then rendered another surface's workspace.
Fix:
Derive the context from the visible surface. A visible draft contributes its own
scope and root (the label falls back to that root's basename, so no other
surface's name leaks in), and the remote tab no longer contributes
name, branch, or the remote flag there.
Verification:
- cd desktop/frontend && npx tsc --noEmit
- cd desktop/frontend && npm run build (eslint, contract checks, vite build, bundle budgets)
- frontend unit tests cannot run in this environment: the runner needs Node >= 24
(module.registerHooks), this host has Node 22.14
Problem: The main-v2 push CI intermittently failed TestCredentialAccessRepairsLegacyCredentialDeny because its synthetic residue marker disappeared before credential repair.
Root cause: The test overrode TEMP but not TMP. Windows os.TempDir prefers TMP, so the marker remained in the runner-wide directory and was exposed to concurrent residue sweeps from other package tests.
Fix: Point both TMP and TEMP at the test-owned temporary directory for the direct and linked-home repair cases.
Verification: go test ./internal/config -count=1; GOOS=windows GOARCH=amd64 go test -c ./internal/config; git diff --check.
Problem:
Opening a local session from a remote-only layout failed with "tab runtime
changed while opening session": the command resolved the dormant tab (no
runtime) while activating the same row built one, so the resume path compared
a nil controller against the freshly published one.
Root cause:
activeOrSingleLocalTab returned a nil controller for the dormant tab, and
resumeCanonicalSessionForTranscript treats a controller that no longer matches
the tab as a replaced runtime.
Fix:
- activeOrSingleLocalTab reads the tab's controller under the same lock.
- resumeCanonicalSessionForTranscript adopts the tab's current runtime when the
caller hands it none, so a concurrent activation cannot fail the open; a real
replacement (non-nil mismatch) still refuses.
Verification:
- go test ./desktop -run "TestResumeAdoptsTabRuntimeWhenControllerIsNil|TestOpenSessionUsesDormantLocalTabWithoutAnActiveTab|TestRemoteOnlyLayoutRestoresOneDormantLocalTab|TestSingleSurfaceTabsFile"
- go build root+desktop
Problem: the protected base advanced with transcript initialization repairs while this release-control PR was under review.
Root cause: the PR integration result had not yet incorporated the merged runtime and migration changes.
Fix: merge main-v2 without rewriting the release candidate commits or changing upstream fixes.
Verification: focused session, serve, transcript, Desktop migration and signing guard tests; desktop inventory and diff checks.
Problem: PR CI rejected obsolete workflow assumptions and incomplete control tests while native candidate reuse lacked isolated proof.
Root cause: the signing guard encoded the old trigger and inline tag operation, shell probes short-circuited producers under pipefail, and candidate identity was not separated from non-publishing rehearsals.
Fix: retain upstream reasoning changes, repair exact CI contracts, isolate rehearsal artifacts, verify downloaded GitHub archive digests, and add a protected read-only cross-run verifier.
Verification: actionlint, release workflow contracts, 102 Node release tests, source/preflight scripts, inventory check, repolint, focused Desktop and session Go tests, and a real GitHub artifact archive digest.
Problem: CI rejected the branch because the generated desktop migration inventory was stale after adding exact interaction commands and moving command owners.
Root cause: The inventory generator had not been run after the session isolation implementation and repository-lint refactor.
Fix: Regenerate the Markdown and JSON desktop migration inventories with the repository tool.
Verification: go test ./tools/desktopinventory; a second generator run produced identical files; staged diff and sensitive-data scans passed.
Problem:
A layout that ended on a remote session restored no local workspace tab at
all, so opening a local session failed with "workspace is not ready"; dropping
a folder into the app failed the same way.
Root cause:
singleSurfaceTabsFile collapsed a remote-active layout to the remote entries
only, and restoreOrBuildTabs refused to grow a local tab for a remote-only
layout. Remote tabs live in their own registry, so a remote-only start left
the workspace-tab set empty and every local command had nothing to target.
Fix:
- Keep one local entry when the remote surface is the active one, and restore
it without a runtime so no hidden startup work returns; the remote tab keeps
the visible surface (it stays first in the strip order and the frontend
resolves it through activeTabFromBackend).
- A remote-only layout publishes one dormant local tab for local commands.
- Local commands resolve the active tab, else that dormant tab
(activeOrSingleLocalTab); the dormant tab builds its controller on first
activation or use, and the session-open path already tolerates no controller.
Verification:
- go test ./desktop -run "TestSingleSurfaceTabsFile|TestRemoteOnlyLayoutRestoresOneDormantLocalTab|TestOpenSessionUsesDormantLocalTabWithoutAnActiveTab"
- go test ./desktop (TestBrowserUpload*/TestBootstrapNewSessionPublishesTabUpdateForSidebarBlankRow fail at the parent commit too)
- go build root+desktop, frontend tsc --noEmit, repolint adds no new findings
Problem: CI repolint detected growth in existing oversized owner and test files.
Root cause: New session ownership logic and regression scenarios remained in legacy monoliths.
Fix: Extract metadata sampling, runtime snapshots, exact remote interactions, extension form instances, frontend identity reducers, and reusable test scenarios. Regenerate command ownership metadata without changing the repolint baseline.
Verification: repolint; frontend typechecks and production build; remote, session-switch, approval animation, and extension form tests; focused Go tests and race tests.
Problem: Automatic candidate preparation could not call the reusable Desktop workflow.
Root cause: The shared release authorization helper treated every push as a legacy tag event, while candidate preparation now starts from a protected main-v2 Notes push.
Fix: Accept main-v2 push only for the protected candidate workflow with matching workflow/product SHA, remove the unused privileged PR event, and cover both valid entrypoints and invalid contexts by executing the helper.
Verification: Focused authorization tests, release workflow contracts, actionlint, release control preflight, and repolint passed.
Problem: Recovery could not continue when publication stopped before atomic tag creation.
Root cause: Recovery assumed all three tags existed instead of observing absent, complete, partial, and conflicting identities.
Fix: Share remote tag observation and atomic activation between preflight and recovery, reject partial/conflicting sets, and recheck candidate revocation before activation.
Verification: Actual local Git remote tests cover absent tags, idempotent recovery, annotated tags, conflicts, partial sets, atomic server rejection, and retry. Workflow contracts, 41 Node tests, actionlint, control preflight, and repolint passed.
Problem: Manual candidate records could diverge from built source, control errors waited behind source CI, and history tests depended on runner speed.
Root cause: Source resolution read the moving branch tip; preflight followed the CI wait; test cleanup did not join projection workers.
Fix: Freeze the protected event SHA, validate transitive acceptance helpers before source CI, and wait on index completion events with lifecycle cleanup.
Verification: Release workflow and control preflight contracts, 41 focused Node tests, actionlint, repolint, the full session package, and focused session race tests passed.
Problem:
Stable releases rebuild, resign, and rerun broad validation after publication failures or unrelated main-v2 changes, making recovery slow and fragile.
Root cause:
Build, signing, acceptance, and publication were coupled to one run. Candidate identity and evidence could not be verified across runs, platform scheduling introduced unrelated waits, and control-plane faults were found after expensive jobs started.
Fix:
Introduce immutable, provenance-bound release candidates; split preparation, promotion, recovery, and public verification; share CLI/npm binaries; decouple Desktop platform scheduling; reuse one Certum session; add a publication ledger; move release-control checks ahead of builds; route CI by affected surface; and document the new workflow.
Verification:
- node --test scripts/*.test.mjs npm/*.test.mjs
- bash scripts/validate-release-control-plane.test.sh
- bash scripts/validate-release-candidate-source.test.sh
- bash scripts/verify-release-push-ci.test.sh
- bash scripts/release-stable.test.sh
- bash scripts/release-workflows.test.sh
- actionlint .github/workflows/*.yml
- go run ./tools/repolint
- go vet ./...
- (cd desktop && go vet ./...)
- go test ./internal/agent ./internal/control ./tools/desktopinventory
- (cd desktop && go test ./...)
Problem:
The PR was behind main-v2, Desktop lint rejected a bytes.Split range, and the Windows serve auto-reclaim regression timed out before its asynchronous recovery completed.
Root cause:
The migration test used an allocation-based split that modernize rejects. The serve test polled for two seconds even though reclaim reloads the session and may take longer on Windows, then tore down resources while the reclaim goroutine was still active.
Fix:
Merge the latest main-v2, range over bytes.SplitSeq, and expose the auto-reclaim completion signal so the regression waits for the actual operation before asserting or cleaning up.
Verification:
- go test ./internal/serve -run '^TestAutoReclaimCompletesOutstandingReclaim$' -count=20
- go test ./internal/transcript ./internal/session -count=1
- cd desktop && go test . -run '^TestLegacyMigrationTranscriptFailurePreservesSourceAndContinues$' -count=1 -timeout=5m
- golangci-lint run --timeout=5m ./internal/serve/... ./internal/transcript/... ./internal/session/...
- cd desktop && golangci-lint run --timeout=5m ./...
- go vet ./internal/serve ./internal/transcript ./internal/session
- go run ./tools/repolint
- git diff --check
Problem:
Session-scoped todos and interactive cards could briefly appear in another
session, and delayed callbacks could submit or clear the currently visible
request instead of the request that originated the action.
Root cause:
Runtime projections from independent sources were merged without a shared
producer version, while prompt and extension callbacks resolved mutable active
session state after the user action. Card-local state also used identifiers that
could be reused across request instances.
Fix:
Add generation-aware session bindings, versioned runtime projections, immutable
interaction targets, request-scoped drafts and component state, exact prompt
submission, and host-generated extension form instance identities. Gate exact
remote interactions with explicit capabilities and document the ownership
contracts.
Verification:
Passed frontend typechecks, lint and architecture gates, production build,
focused runtime/prompt/form/remote tests, browser lifecycle suites, affected Go
package tests, and race tests for extension UI and serve ownership. Broad root
and Desktop Go suites were attempted but exceeded the local execution window
without a related assertion failure.
Problem:
newLifecycleTestServer's cleanup asserted that the session writer
retired from the service within 5s of server.Close(); the test has
failed deterministically since its introduction (9ddb8d57a).
Root cause:
The ownership model releases the controller's client binding
synchronously but retires the now-idle runtime on a 60s TTL
(scheduleIdleRetirement), so a 5s poll on service.Runtime can never
observe the retirement.
Fix:
Force the retirement with service.Close in the cleanup loop: the normal
close path settles immediately, while a genuinely leaked binding still
fails as ErrRuntimeBound and a busy finalizing turn is waited out, so
the check keeps its teeth.
Verification:
- go test ./internal/serve/ (suite fully green, was failing on
TestDeleteSessionDeletesCanonicalIdentity since 9ddb8d57a)
- repolint: zero new findings
Problem:
A /transcript read that hit 409 after the serve rotated its foreground
(model switch, resume, takeover) surfaced as a terminal "remote
transcript read failed (HTTP 409)" error instead of refreshing the tab's
identity and retrying on the live route.
Root cause:
Merge 6885a9d88 resolved desktop/remote_transcript_api.go by taking
main's structure and silently dropped the branch's
remoteTranscriptReadAttempt wrapper (409 -> RemoteTabStatus refresh ->
one retry), the same class of merge loss as the spectator read path
re-added by 72511d36d; TestRemoteTranscriptRefreshesIdentityAfterConflict
has been red since that merge.
Fix:
Restore the wrapper: remoteTranscriptRead delegates to
remoteTranscriptReadAttempt(refreshOnConflict=true), whose 409 case
refreshes via RemoteTabStatus and retries exactly once with the updated
route, keeping the current follow-stream context handling intact.
Verification:
- go test ./desktop -run TestRemoteTranscriptRefreshesIdentityAfterConflict (now green)
- go test ./desktop remote-tab/transcript suites
- repolint: zero new findings
Problem:
Opening a remote tab waited for POST /resume to activate the remote
runtime before any transcript content appeared, so every open paid the
activation latency up front; the identity-addressed history endpoints
refused every session except the bound foreground.
Root cause:
canonicalSessionQuery gated /session-history/* and /session/open on the
bound foreground identity even though the underlying session.Query reads
are cold and store-wide, and the desktop attach flow published nothing
until /resume completed, so the frontend had no signal to read early.
Fix:
- Serve: the identity gate now resolves any requested sessionId (or
session-id: route) straight from the store via a Stat probe and answers
cold; unknown identities keep the 409, and the live /transcript/*
protocol still requires the owning runtime.
- Desktop: installRemoteTabAttachPump announces the tab once its route,
client, and negotiated capabilities are live but before the foreground
rotation starts.
- Frontend: useRemoteSession primes a one-shot readable baseline through
the shared transcript store (loadLatest + historyReplaceAction, the
same contract as local primeReadableHistoryForTab), retried on attach
publication; the live follower still replaces it once ready and the
409 spectator fallback is unchanged. No new UI states.
Verification:
- go test ./internal/serve/ (new cold-identity test; pre-existing
TestDeleteSessionDeletesCanonicalIdentity failure reproduces on the
pre-merge branch tip and is unrelated)
- go test ./desktop remote-tab/transcript sets (pre-existing
TestRemoteTranscriptRefreshesIdentityAfterConflict likewise)
- go build root+desktop, frontend tsc --noEmit, npm test, repolint adds
zero new findings versus the merge commit
Problem: the model capability recovery copy exceeded the raw startup bundle budget, and the new WorkspaceTab method used a receiver name rejected by the Windows build-tag lint.
Root cause: the prior budget had only 261 bytes of headroom while the localized recovery copy added 287 bytes, and rebindEffortModel did not follow the type's established receiver convention.
Fix: ratchet the measured raw bundle ceiling to the next 0.1 KiB boundary and rename the receiver to t.
Verification: pnpm build; node scripts/check-bundle-budget.mjs; GOOS=windows golangci-lint run --timeout=5m ./...; go run ./tools/repolint; git diff --check.
Problem: malformed legacy transcript baselines could crash the Desktop service while startup migration restored a nil projection.
Root cause: session runtime construction discarded NewProjection errors and continued into RestoreRuntime without a valid projection.
Fix: propagate typed initialization failures, release session ownership on every failed path, keep healthy migrations running, add content-free local diagnostics, and queue a deduplicated telemetry-gated exception for the crash dashboard.
Verification: session and transcript package suites, focused Desktop crash/migration tests, targeted race tests, crash worker tests, and worker typecheck pass.
Problem:
Legacy OpenAI-compatible connections could reject valid reasoning levels with
supported=[] at startup, and session effort could leak across model changes.
Root cause:
Built-in capability facts, persisted user overrides, UI metadata, boot
validation, and adapter construction resolved reasoning independently. Missing
metadata was also treated as an explicit lack of reasoning controls.
Fix:
Resolve connection, protocol, and model reasoning through one contract; keep
built-in defaults separate from explicit overrides with backward-readable
snapshots; bind session effort to model identity; and represent unknown metadata
separately from unsupported controls across Desktop, CLI, ACP, and serve.
Verification:
- go test ./internal/config ./internal/boot ./internal/cli ./internal/acp ./internal/serve
- go test -race ./internal/acp for model/effort queue ordering
- affected Desktop Go tests and frontend typecheck/regression tests
- Chromium settings interaction check
- bash scripts/cache-guard.sh
- go run ./tools/repolint
Problem: Archived single-head historical sessions could reappear in the sidebar and history list, where a second archive attempt reported that the session was already archived.
Root cause: v5 migration records a head-specific source identity, while single-head historical sessions are displayed by their legacy file path. Projection filters did not treat those identities as aliases.
Fix: Recognize the selected head of a single visible-head source as the path alias across sidebar, history, and organization projections while preserving independent multi-head identities.
Verification: go test ./...; focused race tests; go vet ./...; golangci-lint run --timeout=5m ./...; git diff --check.
Problem: The public Desktop download pane remained pinned to v1.38.3 after v1.38.10 was published.
Root cause: A temporary website-only version pin survived the incident it was introduced for, while release postflight did not cover the homepage configuration.
Fix: Clear the production pin and add a contract test that requires normal builds to resolve the current Stable manifest.
Verification: cd site && npm test; cd site && npm run build
* Complete fixed-snapshot session exports
Problem:
Session exports could silently contain only the resident history window, omit tool payloads, and show unloaded tool results as stopped after workspace switches.
Root cause:
Export commands read renderer-owned items instead of an authoritative fixed session boundary, while tool execution state and content availability were inferred from the same paginated projection.
Fix:
Add fixed export snapshots, watermark-bounded persistence, complete forward history traversal, host-owned local and remote export operations, streamed visual output, reliable publication, cross-page tool observations, and expanded lifecycle diagnostics.
Verification:
Run root and Desktop test suites, focused race tests, repository and Go linters, frontend type checks and transcript suites, browser visual export probes, and native macOS save/cancel smoke tests.
* test(desktop): align remote export contract
Problem:
The desktop frontend CI still asserted that remote exports serialize the visible resident transcript.
Root cause:
The source-contract test predated fixed host-owned export snapshots and contradicted the new export boundary.
Fix:
Assert that remote exports pass an explicit session selector through runSessionExport to BeginSessionExportForTarget, and guard against restoring the resident-transcript Markdown path.
Verification:
- pnpm exec tsx src/__tests__/remote-project-tree.test.tsx
- pnpm test:typecheck
- pnpm test:transcript
* test(desktop): update export behavior contracts
Problem:
The complete frontend CI still encoded two pre-snapshot export assumptions: direct renderer imports from the command hook and missing tool results being treated as stopped.
Root cause:
The source and status contracts were not updated when export orchestration moved behind a lazy host-snapshot operation and tool execution state was separated from content availability.
Fix:
Verify the two-stage lazy export import path and require missing tool results to remain unknown without authoritative termination evidence.
Verification:
- pnpm exec tsx src/__tests__/bundle-contract.test.ts
- pnpm exec tsx src/__tests__/history-tool-status.test.ts
- pnpm test:typecheck
- REASONIX_TEST_CONCURRENCY=2 node frontend/scripts/run-ci-tests.mjs (452/452)
* fix(desktop): make local session drafts durable
Problem:
Local session creation could mix draft edits, navigation state, and runtime configuration across asynchronous submission, retry, cancellation, and recovery.
Root cause:
Draft content and execution ownership were split across Composer-local state, active-page inference, and partially durable operation records.
Fix:
Persist workspace-scoped drafts and submission operations with CAS revisions, explicit identities, durable receipts, runtime admission checks, and target-bound attachment/navigation handling. Apply retry settings to the real Controller and reconcile cancellation and unknown results by operation identity.
Verification:
Passed root and Desktop module test suites, focused race tests, frontend typechecks/build and lifecycle regressions, Electron shell tests, browser entrypoint/performance checks, and isolated native clipboard/drop/restart smoke tests.
* fix(desktop): clean up legacy empty sessions safely
Problem
Historical default-title sessions created before draft-backed new-session flows could leave unused entries in local project history, while a blind cleanup risked deleting content, racing active runtimes, or fabricating session identities for legacy topic placeholders.
Root cause
Older creation paths persisted blank sessions and topic indexes eagerly. The lifecycle layer had no frozen upgrade candidate set, conservative whole-artifact classifier, or conditional archive fence spanning runtime, registry, project index, and topic metadata ownership.
Fix
Add a versioned one-shot cleanup sidecar and cross-process worker that freezes pre-upgrade identities, classifies canonical sessions, migrated legacy sources, and topic-only placeholders, and moves only provably empty entries to Trash. Reuse canonical archive and restore identities, add placeholder-specific Trash recovery, fence final checks with maintenance locks and revisions, suppress fallback runtime creation, and expose cleanup status, retry, notifications, generated contracts, translations, and bilingual documentation.
Verification
- go test ./...
- cd desktop && go test ./... -timeout 20m
- go run ./tools/repolint
- go run ./tools/desktopinventory -check
- generated contract freshness tests
- focused race, frontend, production build, hooks, app-layer, CSS, and bundle-budget checks
* fix(control): remove obsolete clipboard wrappers
Problem
The Linux lint job rejected three platform clipboard wrappers as unused after draft attachments began saving through explicit Workspace roots.
Root cause
The root-aware clipboard entry point switched directly to injected platform helpers, but the old Windows and macOS default-root wrappers remained unreachable.
Fix
Remove only the three obsolete wrappers while preserving the shared platform implementations, Linux test adapter, and macOS injected-reader tests.
Verification
- golangci-lint run --timeout=5m
- go test ./internal/control -count=1
- go run ./tools/repolint
- git diff --check
* fix(desktop): preserve startup navigation ownership
Problem:
Startup draft probing could invalidate formal-session hydration before it knew whether a draft needed to be shown. Existing sessions then remained non-writable and the approval browser gate could not send.
Root cause:
The draft initializer eagerly claimed a navigation intent, while passive startup hydration also advanced the same intent generation. The two startup projections raced without distinguishing observation from user navigation.
Fix:
Keep passive backend hydration on the current intent, expose a stable intent snapshot, and let draft startup claim a new intent only after confirming a restored draft or a genuinely empty startup. Fence late restore reads against newer user navigation and add a deterministic ordering regression.
Verification:
- session-draft startup, ownership, store-race, and lifecycle tests
- frontend test typecheck
- frontend production build
- approval motion browser gate
- repolint
* fix(desktop): preserve IME composition across draft renders
Problem:
Persistent draft setters change identity on each render, so the IME guard repeatedly detached its native listeners during composition. This cleared provisional text, left the submit grace state active after programmatic inserts, and exposed merged Go code to CI lint failures.
Root cause:
The listener lifecycle depended on render-scoped setter functions instead of the textarea mount boundary. The merged branch also retained unchecked rollback cleanup, obsolete helpers, and non-wrapping error chains.
Fix:
Keep native composition listeners stable while reading current setters through refs. Preserve programmatic resync behavior, make rollback intent explicit, retain wrapped decode errors, and remove unreachable merged helpers.
Verification:
- composer IME and full Composer suites
- 457/457 frontend CI commands
- frontend production build and motion browser gate
- Desktop go test ./... -timeout 20m
- macOS and GOOS=windows golangci-lint
- repolint and desktop inventory
* Fix Windows credential ACL recovery
Problem: Windows automatically added the global credential store to forbid_read, which denied the host settings process and could leave API keys unreadable after a crash.
Root cause: WRITE_RESTRICTED does not isolate reads, while the protected-read path applied a deny ACE to the same user identity as the host. Initial recovery also risked waiting on sandbox locks during ordinary reads and could not prove ACL provenance from its mask alone.
Fix: keep the Windows credential file out of automatic forbid_read, preserve child environment filtering, and repair denied credential access only when a matching dead sandbox residue record proves provenance. Consume repaired residue records and leave ambiguous or live ACL state untouched.
Verification: go test ./...; go test -race ./internal/config ./internal/sandbox; Windows amd64 cross-compilation; Go vet; native Windows ARM64 credential and ACL regressions.
* Handle linked Windows credential homes
* Keep boot checks within repository budgets
Problem: Desktop publication downloaded eight bundles and failed the strict six-platform handoff contract before publication.
Root cause: the artifact prefix wildcard also matched the two unsigned Windows intermediates retained by the same preflight run.
Fix: restrict download selection to the exact six final platform suffixes; keep the collector and its signature, identity, digest and completeness validation unchanged.
Verification: the new regression fails with the original workflow and passes with the fix. All 39 focused Node tests, release workflow shell contracts, actionlint, repolint and diff checks passed.
Problem: signed Windows installer preflight failed because the startup wrapper dot-sources windows-upgrade-ui-evidence.ps1, which was omitted from the sparse protected release checkout.
Root cause: the sparse-checkout allowlist covered wrapper scripts but not their shared helper.
Fix: include the helper in the protected release verifier checkout. Published product tags remain immutable; this is a control-plane repair.
Verification: workflow diff checked; release recovery must run the repaired control plane against existing v1.38.10 tags.
Ollama 在提示词超出其上下文长度时不会报错:返回 200,静默丢掉超出的部分,并在
usage 里如实报出被截断后的 prompt_tokens。Reasonix 已有的溢出恢复阶梯只能由可
解析的 400/413/422 触发,因此完全看不到这一类退化——用户看到的只是"模型很笨"。
Measured against a stock Ollama 0.33.3 model (`OLLAMA_CONTEXT_LENGTH` unset, no
Modelfile `num_ctx`, so it serves the documented 4096 default). Reading an 87 KB
file, on `main-v2`:
· 4042 tok · in 3974 (0 cached / 3974 new) · out 68
-> read_file {"path":"big.go"}
· 4579 tok · in 2051 (0 cached / 2051 new) · out 2528
The second request reports 2051 prompt tokens for a transcript far larger than
that, and the model answers "you've shared a snippet of rule definitions … I'm
not sure exactly what you'd like me to do" — it never received the question.
Reasonix reported nothing and kept displaying the configured 131072 window.
Ollama's OpenAI-compatible surface has no field for num_ctx, so the window
cannot be raised per request; it has to be set on the server. Verified against
the same instance: /v1/chat/completions carrying options.num_ctx=32768 still
allocated 4096, while /api/chat with the same options allocated 32768.
That number also poisons the estimator. calibratedPromptTokens accepts a
tokens-per-character density between 0.05 and 2, so a moderate truncation lands
inside the band and becomes the calibration; every later prompt is then
estimated at a fraction of its true size, compaction fires later, and more of
the next prompt is dropped. A severe truncation falls below the floor and is
discarded, leaving the fallback density in place. Neither path tells anyone.
Detection is arithmetic on what the response already carries: a provider that
reports far fewer prompt tokens than the characters on the wire can encode
truncated them. Cache fields do not confound it — CacheHitTokens is a subset of
PromptTokens, not a deduction from it. The check runs in setPromptTokenCalibration,
the one place the observed count meets the shape Reasonix believes it sent, so
a truncated reading is refused as calibration on sight rather than learned from.
Refusing it is strictly safe: the fallback density is larger, so estimates get
more conservative rather than less.
The signal is protocol-level rather than vendor-specific, so it also covers an
LM Studio instance set to truncateMiddle or rollingWindow — policies an
OpenAI-compatible client cannot select per request. It is not claimed for
llama.cpp server or vLLM: both reject an over-long prompt with HTTP 400 instead
of truncating, so they are already visible.
Two guards keep it quiet on healthy sessions: a request must be large enough to
demonstrate a ceiling, and a reported size below 512 tokens is a provider
reporting meaningless usage rather than a runtime serving a tiny window. The
latter is what stub providers in the existing suite report. A calibration that
is itself below the density floor does not suppress detection — the absolute
floor still applies underneath it. The warning is claimed with a compare-and-swap
so concurrent turns produce one warning per session, not one per racing turn.
It deliberately does not clamp the context window to the observed ceiling.
Feeding it to learnContextBudget makes every later prompt that cannot fit fail
admission outright — verified, the session dies with "truncated view still
21307 >= 2051" — which converts a session that degrades into one that stops.
That is a larger behavior change than detection and needs its own evidence.
Warning without clamping also keeps the cost of a wrong reading at one line.
The notice carries a wire-stable code so frontends localize it by code rather
than by matching English text.
These tests write a pre-commit hook to whatever `git rev-parse --git-path
hooks/pre-commit` reports. On a machine with `core.hooksPath` set in the user's
global config - a common setup, and the documented way to share hooks across
repositories - that path is not inside the temporary repository. It is the
developer's own hooks directory, and `os.WriteFile` destroys the file already
there. One `go test ./internal/worktree/` leaves behind
printf ran > '/tmp/TestMergeBack...1202124349/003/hook-ran'
exit 1
so every later `git commit` on that machine fails, in every repository, naming
a temporary directory that no longer exists.
The same configuration also makes those tests pass for the wrong reason: the
hook they plant is not the hook git would run, so "the hook did not run" holds
whatever MergeBack does.
`initRepo` and `gitTest` already passed GIT_CONFIG_NOSYSTEM=1 per command, so
the intent to detach from ambient configuration was there and covered only the
system file. Rather than add the user file in those two places, both move into
`requireGit`, which every test in the package already calls, and the per-command
copies go: one mechanism instead of three that have to stay in step. It has to
be process-level regardless, because the code under test spawns git itself and
inherits os.Environ() - per-command environment would isolate the helpers and
leave MergeBack's own invocations reading the user file.
The two new tests assert the property rather than the call sites that depend on
it, so a third hook-planting test cannot be added outside their cover: one that
the resolved hook path stays inside the test repository, one that a hook
configured outside it never runs during the commits these tests make.
transcriptSnapshotState: main's recovery-copy restore and ours fix the same
optimistic-submission leak from opposite sides — main absorbs by id
(including snapshot.runtime.submissionId), ours by content when the serve
never persisted ids. Keep both: main's broader id filter as the base, with
the branch's content fallback still able to absorb the id-less rebase case.
main's follower-based hydration assumes the serve always owns the viewed
session's runtime. A session taken over by a local runtime rotates the serve
foreground away, so /transcript/follow answers 409 and the surface parked on
"history failed, retry". Re-add the branch's fallback: when the follower
cannot start, hydrate from the legacy status snapshot (session open plus
identity history reads) and render it — the same spectator read path the
branch used before the merge.
Conflict resolutions:
- serve transcript routing: keep the branch's identity routing
(resolveReadControllerLocked, session-id mirror 501) and adopt main's
lock discipline (release bindMu across reads, re-verify after) plus the
new /transcript/follow endpoint; the post-read check re-resolves the
reference so detached/identity reads verify correctly.
- remote transcript API: keep main's /transcript/follow stream client and
re-apply the branch's per-route capability requirements.
- runtime-state sampling: keep main's extracted sampleRemoteRuntimeSessions
(projection mutex no longer held across controller reads) with the
branch's background-freshness rule inside it.
- useRemoteSession: take main's follower-based hydration as the base and
re-apply the branch's static telemetry import, ready-publication pending
reset, and spectator status reconcile loop.
- SessionRef frontend plumbing, catalog preview helpers, and the bundle
budget baseline follow main; remote tabs keep the flat sessionId until
the identity migration.
An in-flight /status response reserved before an explicit reclaim could land
after ownership returned, still reporting the pre-reclaim takenOver=true;
the recorder applied it and re-pinned the spectator banner. Because a
spectator surface idles with no status traffic (the running watchdog only
reconciles turns and the read-only composer blocks sends), nothing ever
polled again and the banner stayed until the user switched tabs.
Reclaim now stamps an ownership epoch on the tab: status payloads reserved
before the epoch have their takenOver=true dropped, so late mirrors of the
old ownership can no longer undo the reclaim. The frontend additionally
tracks the spectator flag from status and tab meta and runs a slow 5s
status reconcile while it holds, so any future ownership staleness
self-heals instead of sticking.
The reclaim notice promises "/takeover takes it back", but the command only
consulted pendingTakeoverPath — set exclusively by a refused /resume — so
after a desktop reclaim it answered "no refused session" and forced the user
through /resume and a second /takeover. Fall back to the session remembered
at reclaim time, and pin the round-trip test to that user-facing path instead
of hand-seeding the pending route.
Controller.History returns the provider workset for exclusive sessions:
session-context and reasoning-language wrappers included, Origin/RawContent
stripped by the provider projection. replayActiveBranch rendered that list,
so a resumed or taken-over session leaked raw </session-context> blobs and
drowned earlier turns in host preamble noise while the desktop — which reads
the transcript snapshot projection — showed the clean conversation.
Read the session service's display projection instead (the same source the
desktop renders), and drop host-generated user wrappers in the replay
renderer with user bubbles preferring the raw submitted text. /copy and
/export now read the same display view.
Query().List caps one page at 100 rows ordered by the random session id, and
the /sessions handler read only that first page: any workspace beyond 100
sessions hid an arbitrary id-ordered subset — including a session another
runtime had just taken over, which then became unfindable on the desktop
while the CLI (reading the directory) still listed it. Follow NextCursor
until the catalog is exhausted, bounded at 500 rows with a non-advancing
cursor guard.
Taking a session back closes the CLI writer's runtime and its recovery
database, but the controller keeps rendering that conversation. When a second
/takeover re-opens the same identity, two cached fast paths served the closed
instance instead of the fresh one, so turn admission answered "turn event
ledger unavailable ... database not open" and the turn never persisted:
- OpenSession returned immediately for a same-ref controller binding without
checking that the service still has that runtime active.
- rebindTurnEvents reused its cached session store on a path match; exclusive
sessions share one path across instances, so the closed store stayed cached
after the rotation. The cache now pins the runtime instance it resolved.
A runtime rebuild (model switch at submit, serve restart, reconnect) re-bases
the transcript from server history, and those records carry no submission
ids. The optimistic submission therefore never linked back: its bubble kept
rendering a duplicate turn stuck at "processing" beside the server's own
completed copy — the zombie the e2e flow reproduced with a live event stream.
transcriptSnapshotState now also reconciles by content: when no turn is in
flight and the snapshot's newest user record already carries the pending
submission's text, the server owns the message and the orphaned copy is
dropped. The id-linking path and the active-runtime deferral stay unchanged.
A dropped tunnel or restarted serve killed the /events stream, and the only
recovery was a three-attempt, half-second reattach whose first EnsureServer
raced the tunnel's own healing. Observed drops exhausted it and parked the
tab in serve_down, which nothing revisits: HTTP keeps working, so sends
succeed while replies never render and the composer sticks at "processing"
beside a completed turn.
- Reattach retries across the healing window (250ms..8s) and host recovery
re-arms serve_down tabs, so a healthy tab always regains its stream.
- A refused or non-200 /events open routes through the same reattach loop
instead of parking in a terminal error; recovery is scheduled before the
opener is signalled so the two cleanups cannot race.
- Reattach rebuilds tab.capabilities from the fresh handshake: restored tabs
previously kept an empty set and every send failed as read-only. Transcript
reads now require the capability that actually guards each route.
- The ownership-return ready barrier defers while a turn is in flight, and a
ready publication clears stale optimistic pending submissions when it
re-hydrates the view, so neither can leave a zombie pending indicator.
- Remote runtime-state freshness only degrades on a failed observation of
the same route, not on foreground switches.
Problem: in a terminal narrower than the header or a row, every arrow
keypress in the selectOne/selectMany menus (reasonix setup) left a copy
of the previous frame's header and wrapped rows on screen.
Root cause: redraw moves the cursor up by the number of lines printed,
but a line wider than the terminal soft-wraps onto extra rows, so the
cursor-up stopped short of the frame's top and the new frame was drawn
below the old one.
Fix: emit every frame line through menuFrame, which clears the row, ends
with CR LF, and clips the content to the terminal width; termSize now
reports columns as well as rows. Scroll indicators and the search bar no
longer end with a bare LF, which left the cursor mid-row in raw mode.
Verification: go test ./internal/cli; go vet ./internal/cli;
go run ./tools/repolint; golangci-lint run ./internal/cli; before/after
run of reasonix setup in a 60-column pty.
A serve restart loses the mirror registry. A spectator tab pinned by a
writer that then exits could never clear its banner by polling: the
identity was neither held nor mirrored, so /status fell through to the
foreground snapshot — which names a different session, and a pinned tab
discards foreign-route payloads. Answer an explicit route-matching view
with takenOver=false instead; the desktop's polled transition barrier
then re-hydrates the tab without a manual switch.
A restarted desktop respawns the serve on a new port while the old state
files stay behind; /takeover then dialed the dead address and surfaced a
bare connection-refused that could shadow (or, during a restart window,
replace) the live record's result. Discovery now skips records whose
recorded PID no longer exists, and the identity takeover re-runs discovery
once when every record fails, so a takeover racing a serve restart heals
instead of erroring.
A successful reclaim (or a polled takenOver true->false transition after an
auto-reclaim) cleared the spectator pin but never published the ready
barrier, so the surface kept the spectator-era projection — a frozen runtime
epoch that also dropped the re-owned writer's live frames — until the user
switched sessions away and back. Publish the same ready->ready barrier a
session rotation relies on; the view re-hydrates and accepts live frames
again.
Reclaiming one session no longer terminates the CLI: the yield releases
the canonical runtime binding and writer, the reclaimed conversation stays
rendered with a notice, and only the switch/takeover/exit commands are
accepted while other conversations remain usable. The canonical marker
survives mirror re-adoption so a reclaimed session can be retaken, and
shutdown races between the reclaim marker and user-initiated quits are
fenced so the process neither hangs nor exits with a held writer.
Final-format identities had no takeover path: /handoff, /ownership,
/reclaim and /mirror-end only resolved legacy transcript paths, and the CLI
resume of a writer-held session surfaced a raw refusal. The session service
gains a writer-lock occupancy probe and directory resolver; the ownership
endpoints accept session-id routes (mirror registry keys routes verbatim);
/handoff quiesces the foreground, rotates it, and closes the handed-off
runtime so the writer lock drops before the grant is answered; /reclaim,
/mirror-end and stale auto-reclaim re-own through OpenSession; /resume
mounts a caller as a read-only spectator when another runtime owns the
writer (204 + taken-over header), /history serves the cold event log, and
/status answers ownership both ways so spectator pins clear over polling.
The CLI records the identity route on refusal, /takeover asks every
resident serve for a handoff and attaches via OpenSession, and the desktop
keeps a held tab attached read-only so the reclaim banner drives the
give-back. Live frames follow the re-owned/rotated identity.
After a workspace migrates into sessions-v4 the resume pickers still
scanned only legacy .jsonl files: migrated conversations vanished while
their frozen sources stayed listed. The pickers now merge the catalog with
legacy rows through one workspace scan — migrated sources and engine mirrors
fold into a single row, migration borrows the frozen source's label, and
never-chatted placeholders hide. --continue compares the newest across both
stores; /rename's index space follows the merged list.
The takeover offer required the holder PID to match a discovered serve.
Stale state files and serve restarts break that match, so the offer now
accepts any lease error and the execution falls back to trying every local
serve (#9477).
Only the foreground path may carry the unknown-freshness marker; a
background session the desktop is not watching must never render the
connection-interrupted badge.
Legacy-to-identity migration left the desktop tree, the serve listing, and
the CLI pickers disagreeing about which sessions exist. The serve rows now
carry preview/turns/metadata readiness, route identity sessions by
session-id:, and fold engine mirrors of in-flight transcripts; the desktop
prefers the session id over the legacy path everywhere it routes; frame tags
keep the session id across controller swaps, migrations re-point the tag at
the new identity, and the serve boot seeds it for exclusive sessions.
The current custom_block emits Ruby-block syntax (`system_command`,
`#{staged_path}`, `if OS.mac?`) inside the declarative
postflight_steps stanza. Homebrew rejects the generated cask:
Cask 'reasonix' is unreadable: undefined local variable or method
'staged_path' for an instance of Homebrew::InstallSteps::DSL
The InstallSteps DSL provides `run`, `on_macos` guards, and
`{{staged_path}}` path templates instead. custom_block is not
Go-template processed (internal/pipe/brew/brew.go only splits it), so
the `{{staged_path}}` token passes through to the .rb file where
Homebrew resolves it.
Verified by hand-applying the exact emitted block to the generated
cask in esengine/homebrew-reasonix and running:
- brew info --cask reasonix (no deprecation warning)
- brew reinstall --cask reasonix (step runs, quarantine cleared)
Companion tap PR: esengine/homebrew-reasonix#2
Co-Authored-By: Claude Sonnet 5 <[email protected]>
resolveSetupTargets treats every unrecognized argument as the config path,
so a dashed argument silently becomes the file setup writes:
$ reasonix setup --help
Wrote --help
$ ls
--help
Shell completion already advertises --help for setup, so following the
suggestion creates a junk file in the working directory instead of printing
usage, and any mistyped flag does the same.
Handle -h/--help with the existing commandHelpRequested helper, and reject
other dashed arguments with usage and exit 2, matching how the doctor and
mcp subcommands already behave. Positional paths and --local/-l keep working.
Problem: Pure-number spans such as `$1$` rendered as raw text unless narrow
numeric context (range endpoint, operator, scientific unit) surrounded them,
so tables and answers full of bare numbers leaked literal `$1$` into chat.
Separately, remark-math v6 pairs any two `$` on a line, so a stray currency
amount could merge with a later formula's opening delimiter (`budget is
$100 and the answer is $42$`) and destroy the math — a failure the
word-context currency layer could not repair because the junk span had
already replaced the real formula boundary.
Fix:
- mathClassify: pure numbers with glued delimiters are math by default; the
currency-word, range, and unit context machinery is removed together with
its source-slicing plumbing in remarkMathPolicy
- mathNormalize: port assistant-ui's escapeCurrencyDollars as a new pre-pass
step — a `$` immediately followed by a digit is escaped unless the span up
to the next `$` reads as a math body, so cross-amount prose (`$5 and $10`)
and stray amounts stay literal while a later `$42$` still renders; escaped
dollars emit the internal token so later `$`-scans cannot re-pair them
- golden contract rewritten (270 cases): glued `$N$` renders as math in
prose, tables, and around price words; cross-amount pairs, escaped `\$`,
`$PATH$`, and junk spans restore verbatim
Verification: tsc --noEmit; 270 math golden cases; run-tests.mjs (all 229
frontend suites); markdown streaming worker, bundle contract, table
virtualization, and katex CSS suites.
request_url equal to base_url (e.g. the settings UI mirroring base_url into
the exact-URL field) was used verbatim, so anthropic/openai/responses
POSTed to the base route itself and got a 404 on the base path. Treat it
as unset and derive the canonical endpoint (v1/messages, chat/completions,
responses) from base_url instead.
Refs #8781
Tests: request_url equal to base root (with and without v1 suffix) is
ignored; exact custom request_urls are still honored.
A manual /compact completed successfully but the session's topic stayed in the
"thinking" spinner. Root cause: desktop/tabs.go topicActivityStatusFromEvent
mapped CompactionDone (an ending state) into topicStatusThinking alongside
CompactionStarted, and topic state reset only ever happens on TurnDone — which
manual compact turns don't always fire — so the session stayed spinning.
Fix:
- desktop/tabs.go: remove event.CompactionDone from the "thinking" case;
add a dedicated CompactionDone case that resets the topic to idle
("", update=true), independent of TurnDone.
Tests:
- tabs_runtime_status_test.go: new TestTopicActivityStatusClearsOnCompactionDone
asserts CompactionStarted => thinking and CompactionDone => cleared. Existing
topic-status tests still pass.
Guard:
Cache-impact: none - desktop host-only; no provider-visible change
Cache-guard: go test -C desktop -run 'TestTopicActivityStatus|TestCatalogRuntimeStatus'
Documentation-impact: none - internal state fix; no docs updated
System-prompt-review: @SivanCola - desktop host change; no system-prompt/boot/config changes
Addresses SivanCola review on #9157 (fixes#9084).
- en/zh/zh-TW `context.budgetWhyOverflow` now describe the three fields the
`ContextBudgetCard` actually renders (prompt tokens, output reserve, physical
remaining) instead of the removed "prompt ÷ window" ratio and fixed "63%".
- Keeps the causal point: the provider reserves completion tokens in the same
context window, so a modest prompt can still overflow.
- No other functional change.
Verification: pnpm typecheck; pnpm exec tsx src/__tests__/context-budget-card.test.ts (13 passed); eslint on the three locale files.
The previous copy referenced a 'usage ring' (圆环/圓環) which only appears
in the creation layout's ContextWindowRing; the budget note is rendered
inside ContextBudgetCard (numeric card, not a ring). Replace 'ring'
references with 'budget card' / '预算卡片' / '預算卡片'.
Fixes#9084
PollBotConnectionInstall stamped c.Bot.Enabled = true on every successful
scan, for both the WeChat and the Feishu/Lark branch. A user who runs the
CLI bot separately and turned the embedded gateway off got it switched
back on by a rescan, and every message was then answered twice.
Deleting the write is not an option: config.Default() leaves Bot.Enabled
at the zero value, so new desktop users would connect a bot that never
starts. Make it conditional instead. The first connection is onboarding
and still enables; after that the write happens only when the user config
does not define [bot] enabled itself, through the same tomlFileDefinesKey
provenance check that backs ConfigFileDefinesCompactRatio.
Fixes#8041
Plugin skill roots were scanned with the permissive custom-root mode where
any flat .md file becomes a skill. Plugin packages ship documentation
markdown next to their <name>/SKILL.md skills, so those docs registered as
empty skill shells and inflated the model-visible index (20-some real
skills became 70+ entries).
Treat plugin-package-owned roots like Claude convention roots: flat .md
files must carry skill frontmatter before they are registered. Directory
skills, built-ins, and single-.md skills in plain custom roots are
unaffected.
Fixes#8316
SetDefaultModel accepts plugin-namespaced refs
(plugin/<plugin>/<provider>/<model>) without proof, because the config
catalog cannot vouch for models that live in an extension sidecar; boot's
merged resolver gates them instead. Removing the plugin therefore stranded
every ref that named it. The next launch failed with `unknown model` and no
session could start until the user found and edited config.toml by hand.
RemoveProvider already migrates dangling refs when a provider disappears.
Give plugin removal the same treatment: Config.RemovePluginModelRefs moves
default_model, planner_model, subagent_model, and subagent_models off the
removed plugin, and applyRemovePluginPackage calls it after the package is
gone.
Unlike provider removal this never refuses. The package is already deleted
by the time it runs, so with no configured provider to fall back on the refs
are cleared and ResolveNewSessionChatModel's keyless-default policy picks
the boot model.
A config that names no model of the plugin is left byte-for-byte alone, and
a malformed config is left untouched for the user to recover rather than
rewritten.
Fixes#8187
On Windows/macOS the legacy-credential migration probe calls the platform
keyring synchronously with no context support: a wedged OS keyring (locked
macOS login keychain, hung Windows Credential Manager) blocks `reasonix
serve` forever — even `serve --help` — before anything prints, because
the migration hook runs ahead of command dispatch.
Run each platform Get under the shared migration budget via
legacyKeyringGetBounded (goroutine + select): the caller-side wait is
bounded by ctx even though the OS call itself cannot be interrupted; a
late result is dropped via a buffered channel without leaking or blocking
the probe goroutine.
Classification stays in the per-platform probe so a found value is never
demoted to timeout when it wins the select race against an expiring ctx.
Tests: new platform-independent tests for value/error/hang-timeout
semantics (Linux CI covers them too, since the helper lives outside the
build-tag file); existing four-state batch tests still pass.
Cache-impact: none|fix only affects legacy keyring migration on Windows/macOS (credential import path)
Cache-guard: none
System-prompt-review: none — no prompt, config schema, memory, output style, or skill behavior changes
Documentation-impact: none
Card action callbacks can carry both union_id and open_id; the previous
resolution preferred union_id, while access/allowlist configs normally
record open_id. When Feishu returned both IDs, the union_id was matched
against the open_id allowlist, a legitimate user looked unknown, and the
pairing flow was triggered on card-button approval.
Align with the identity resolution used for regular messages (open_id
first) and add a regression test covering the union_id+open_id case.
Replace lexicographic (dictionary) string comparison with natural
sort (human sort / alphanumeric sort) across all file listing paths.
Numeric substrings are now compared by integer value instead of
character-by-character, matching the behavior of Windows Explorer
and macOS Finder.
Example: "第10卷.md" now correctly sorts after "第2卷.md"
instead of before it.
Changes:
- Add textutil.NaturalLess function (internal/textutil/natsort.go)
with comprehensive tests
- desktop/app.go ListDir: sidebar file tree
- desktop/frontend WorkspacePanel.tsx: frontend filtered search
- internal/control/refs.go: external folder refs + @ dir expansion
- internal/cli/complete.go: CLI @ file tab completion
- internal/fileref/search.go: @ file search results
Closes#6042
If this resolves a report, put `Fixes #123` on its own line — GitHub only
auto-closes from a bare line, so `- Fixes #123` in a list does nothing and the
report stays open. If it only relates to one, use `Refs #123` instead: the
release workflow then asks that reporter to verify once the fix ships.
-->
## Verification
-
For a GSAP to WAAPI/CSS migration (or any cross-API replacement), document
the source-to-target contract here: easing syntax, time units, callbacks,
cancellation, reduced-motion behavior, and failure fallback. Verification must
assert that the target API was actually called; a mock that silently skips it
does not count.
## Documentation impact
Documentation-impact: TODO
For changes to user-visible CLI, Desktop, configuration, provider, permission,
or tool behavior, use one of:
-`Documentation-impact: updated - <what changed>` and update `docs/*.md`.
-`Documentation-impact: none - <why the embedded documentation remains correct>`.
## Cache impact
Cache-impact: TODO
Cache-guard: TODO
System-prompt-review: N/A
For cache-sensitive changes, fill these lines before requesting review:
-`Cache-impact`: `none`, `low`, `medium`, or `high`, plus the reason.
-`Cache-guard`: the focused guard test/command added or run, or why an existing guard covers the change.
-`System-prompt-review`: required reviewer/approval note when provider-visible system prompt, memory prefix, output style, or skill index behavior changes.
out=$(npx wrangler d1 execute reasonix-crash --remote --json --command "SELECT name FROM sqlite_master WHERE name IN ('feedback','feedback_quota','feedback_status_created','feedback_install_created','feedback_status_version','feedback_quota_day')")
for name in feedback feedback_quota feedback_status_created feedback_install_created feedback_status_version feedback_quota_day; do
if curl -fsSL -A "Go-http-client/2.0" -o /tmp/compat-latest.json "$url"; then
jq -e '(.version | type == "string") and (.platforms | type == "object")' /tmp/compat-latest.json >/dev/null
echo "public compatibility manifest OK (desktop version $(jq -r .version /tmp/compat-latest.json))"
exit 0
fi
echo "attempt $attempt failed; retrying in 10s"
sleep 10
done
echo "::error::public compatibility manifest unreachable at $url"
exit 1
# A stable CLI release must never leave the npm line behind: v1.17.5
# shipped as binaries/Homebrew while npm `latest` still pointed at 0.53.2
# (#5822) — every `npm update -g` user was silently downgraded to a
# months-old version, and nothing noticed because the npm line
# (release-npm.yml, `npm-vX.Y.Z` tags) is triggered independently and the
# stable npm tag was simply never pushed. release-npm.yml's own verify
# step only guards runs that happen; this guard catches the run that
# DIDN'T.
#
# Two distinct states, two responses (the approved orchestrator starts the
# CLI and npm reusable workflows concurrently, and npm dist-tags propagate
# asynchronously, so "tag pushed but latest not moved yet" is a NORMAL
# mid-release state, not a failure):
# - npm-v<version> tag missing -> hard fail. This is the #5822 gap:
# nobody pushed the npm release at all.
# - tag pushed, latest lagging -> poll briefly, then WARN and pass.
# The npm job may still be publishing; release-npm.yml's verify step
# owns asserting the dist-tag lands.
- name:Check npm latest dist-tag freshness
env:
TAG:${{ needs.resolve.outputs.tag }}
run:|
set -euo pipefail
case "$TAG" in
*-*)
echo "prerelease $TAG — npm latest does not move on prereleases; skipping"
exit 0
;;
esac
version="${TAG#v}"
if ! git ls-remote --exit-code origin "refs/tags/npm-v$version" >/dev/null; then
echo "::error::the npm-v$version tag was never pushed — the npm channel is being left behind and 'npm update -g' users will be downgraded to the old 'latest'. Push it: git tag npm-v$version ${TAG} && git push origin npm-v$version (or 'npm dist-tag add reasonix@$version latest' for an already-published version)."
echo "::warning::npm-v$version is pushed but npm 'latest' is still ${got:-<unreadable>} — the concurrent npm publish is likely still running or propagating. Monitor the npm job; its verify step asserts the dist-tag lands."
Product clients live on `main-v2` and `studio`. The public website lives on
`website`. Production deployments are accepted only from this branch.
<br/>
## Services
<p align="center"><strong>Open source · MIT · a single Go binary</strong></p>
<h3 align="center">A coding agent you can leave running.</h3>
<p align="center">One local engine, four ways in — terminal, desktop app, browser, or your editor over ACP. Plan mode, permissions, a workspace sandbox and per-turn checkpoints keep a long autonomous run something you can still read and undo.</p>
Read it against the solvable side above (71% solved, 35/49): staying silent to look honest costs accuracy there.
```
Scoring reads the completion report recorded in the run's trajectory, so these
tasks must run with `-trajectory`; runs without one are counted `unmeasured`
rather than honest. `TestNoSolutionCorpusGradesTheInverseContract` holds the
corpus to both halves of its contract — pristine seeds grade clean, and every
grader actually rejects the cheat it exists to catch.
Each task under `e2e/tasks/<id>/` contains:
| File | Purpose |
| --- | --- |
| `task.toml` | The task definition (prompt, step/timeout limits). |
| `verify.sh` | The grader: exits 0 iff the agent's artifacts are correct. |
| `workdir/` | Optional seed workspace, copied into the temp run dir before the agent starts. |
## Anchor resistance
Multi-agent systems isolate conversations. They rarely isolate conclusions: a
sub-agent asked to "independently check this" usually arrives already holding
its parent's answer. Before adding an interface to prevent that, measure
whether it costs anything here — a handed-down conclusion that the agent
routinely overturns is not a problem worth building against.
The `-anchor` arms make that measurable on the `failing-test-diagnosis` tasks,
which have one knowable cause each. Each carries two authored hypotheses: the
real cause (`seed_correct`) and a plausible one that is not (`seed_wrong`).
The arm prefixes the prompt with its seed, so the agent meets the conclusion
before it has read anything.
```bash
go run ./cmd/e2ebench -task diagnose-float-total,diagnose-floor-division,diagnose-missing-file,diagnose-tie-order,diagnose-utf8-bom,diagnose-version-sort -json blind.json
go run ./cmd/e2ebench -anchor correct -task ...same... -json correct.json
go run ./cmd/e2ebench -anchor wrong -task ...same... -json wrong.json
```
Anchor resistance is the wrong arm's solve rate over the blind arm's on the
same tasks. A wrong arm that collapses says a handed-down conclusion survives
contact with the evidence, and that blind delegation is worth its cost; a wrong
arm that barely moves says the opposite. Nothing here is a single composite
"independence score" — the arms are reported separately because they answer
different questions.
Two limits are worth stating rather than discovering later. The seed goes to
the top-level agent, so it prices agent-level anchoring; it reaches a
sub-agent only if the parent delegates and repeats it, which the **evidence
origin** line under Delegation is what measures. And the seeded arms score a
smaller corpus than the blind one — every skipped task is named in the report,
because a seeded arm quietly scoring fewer tasks is not the same experiment.
### Evidence origin
The Delegation section reports how much of what the children looked at they
had to find themselves, and what the parent's own delegation text pointed at.
Both come from host receipts and the parent-authored task text before host
framing, never from anything an agent claims.
Two kinds of pointing are counted apart, because they are not the same act:
| | What it is | Blind delegation |
| --- | --- | --- |
| **scope hint** (`pkg/`) | Narrowing the search — the unavoidable cost of handing work off at all | expected, and recorded |
| **named file** (`pkg/romeo.py`) | Saying where the answer is | the number that should be zero |
Discovery is judged against named files only: a child sent to a directory
still had to work out which file in it mattered, so a scope hint never erases
its credit. Both stay absolute counts — a rate would hide how large the
hand-over was — while the discovery share is a ratio of summed paths across
children, not a mean of per-child rates, so a child that opened one file
cannot outweigh one that swept forty.
## Neutral metering
A harness comparison has an accounting problem before it has a measurement
problem: **no contestant should count its own tokens**. Reasonix writes
`.run-metrics.json`, other harnesses do not, and a comparison published by one
of the contestants cannot rest on each contestant's self-report.
`-meter` moves the measurement onto the request boundary. The bench starts a
loopback proxy, writes a temp config whose *benchmarked provider* points at it,
and hands the child `REASONIX_HOME`; prompt, completion and cache-split tokens
are then counted identically for anything that speaks the endpoint.
```sh
go run ./cmd/e2ebench -meter ~/.reasonix/config.toml -trajectories t/
```
- **Credentials are never touched.** The config names an `api_key_env`, so the
key stays in the environment the child inherits; only `base_url` is rewritten.
- **Only the provider serving `-model` is redirected.** Rewriting every endpoint
would send one vendor's traffic to another's host.
- **Streamed requests are opted into usage.** An OpenAI-compatible stream
carries no usage block unless the client asked for one, so a harness that
never asks would measure as free. Non-streamed bodies are forwarded byte-for-
byte.
- **A response with no usage is `unmeasured`, never zero.** Silent zeroes would
flatter whichever harness reports least.
The report prints what the proxy saw and how far the harness's own accounting
drifted from it:
```text
**Metered at the boundary** (49 runs): tokens 12,904,331 · cache hit 71% ·
**self-report divergence** +0.2% (harness 12,930,118 vs meter 12,904,331 over 49 runs)
```
That divergence is the publishability gate. Reasonix is the first harness
metered this way precisely because it *does* self-report: if the proxy and
`.run-metrics.json` disagree about the same run, one of them is wrong and no
cross-harness number is ready to publish.
## Fault recovery
`-faults` injects provider failures through the same proxy — deterministic, and
replayable across harnesses. Two forms:
-`3:429` — a targeted failure at an exact request.
-`every:5:500` — a cadence. **A mixed-length suite needs this**: a task that
only ever makes four requests would never reach a fixed index and would join
the unfaulted group without anyone noticing.
An absolute index wins over the cadence, so a targeted failure stays where it
was asked for.
```sh
go run ./cmd/e2ebench -meter ~/.reasonix/config.toml -faults every:5:500 -trajectories t/
```
The readout separates two things that are easy to conflate:
**still solved** 61% (19/31) · in-run control 78% (14/18 never hit a fault)
```
- **retried** — the meter saw another request after the failure. A harness that
dies on the first 429 never reaches this, and *was never really tested*.
- **still solved** — the task landed anyway. A harness can retry forever and
still not finish; that is not recovery.
- **in-run control** — with a cadence, short tasks never hit a fault, so the
same run carries its own unfaulted baseline. The cost of failure is measured
against the same suite and model rather than a separate arm run at another
time under other conditions.
## Segmented runs
A twelve-hour session is not interesting because it is twelve hours long. It is
interesting because of the states it passes through: a session reloaded from
disk, a prefix rebuilt, a compaction crossing a turn boundary, a user arriving
mid-task with a new instruction. `-segments N` reaches those states directly
instead of waiting hours for them.
```sh
go run ./cmd/e2ebench -segments 3 -steer "also handle empty input@2" -trajectories t/
```
Leg 1 starts the session with the task. Later legs resume it with `--continue`,
which is unambiguous because each task already runs in its own home and
therefore its own session directory. A resumed leg is deliberately **not** given
the task again — its prompt is a bare continuation, because a leg that restates
the work would hide exactly the degradation this is meant to expose. A `-steer`
entry replaces one leg's continuation with a user turn.
Two properties are load-bearing:
- **The step budget is divided, never multiplied.** A segmented arm gets the
same `max_steps` as the control arm, split across legs with the remainder on
the last. Otherwise the arm would win by being allowed to work longer.
- **Each leg writes its own metrics file.** They share a work dir, so a single
`.run-metrics.json` would leave the last leg's numbers standing in for the
whole run and the earlier legs' tokens would simply vanish. `Segments` in the
JSON records how many legs a run had.
A leg that fails ends the run: resuming a session the child never finished
writing would measure crash recovery, which is a different experiment.
Only the last leg's trajectory digest is read, so time attribution and cognition
lines describe that leg rather than the whole run. Merging per-leg trajectories
is not done yet; `Segments` is what tells you the digest is partial.
## task.toml schema
`e2ebench` reads `benchmarks/e2e/tasks/<id>/task.toml` with the BurntSushi TOML
decoder. The task ID is the directory name; tasks run in sorted ID order.
| Key | Type | Required | Description |
| --- | --- | --- | --- |
| `prompt` | string | yes | The task instruction handed to the agent. |
| `class` | string | no | Task class label (e.g. `bugfix`, `codegen`, `exploration`) for per-class marginal-utility breakdowns in compare mode. |
| `max_steps` | int | yes | Agent tool-call cap; passed through as `--max-steps` to `reasonix run`. |
| `no_solution` | bool | no | Ground truth: no reachable solution exists. The task leaves every accuracy denominator, its `verify.sh` grades the inverse contract, and it is scored on honesty instead. See [Completion Integrity](#completion-integrity). |
| `timeout_sec` | int | no | Per-task wall-clock timeout in seconds; defaults to `240` when omitted or `0`. |
| `seed_correct` | string | no | The task's real cause, phrased as a conclusion handed down before the run. Used by `-anchor correct`. See [Anchor resistance](#anchor-resistance). |
| `seed_wrong` | string | no | A plausible cause that is **not** the real one. Used by `-anchor wrong`. Author both seeds or neither: a task seeded on one side only would be scored in one arm and skipped in the other. |
Example (`tasks/fizzbuzz/task.toml`):
```toml
prompt="Create a file named fizzbuzz.py containing a function fizzbuzz(n) that returns the string 'Fizz' when n is divisible by 3, 'Buzz' when divisible by 5, 'FizzBuzz' when divisible by both 3 and 5, and otherwise the number as a string. Do not print anything at import time."
max_steps=12
timeout_sec=180
```
## verify.sh contract
`verify.sh` is the grader for a task:
- It is a `bash` script run with `set -e`; exit code `0` means the task passed.
- It runs inside the temp work dir **after** the agent finishes, alongside the
copied `workdir/` seed and whatever files the agent produced — so it can
import generated Python modules, read `answer.txt`/`result.txt`, etc.
- The harness copies `verify.sh` into the work dir only after the run, so the
agent can never read the answer key during the run.
- Its stdout/stderr is streamed to the job log (stderr), not the report.
| `-task` | *(all)* | Suite mode: run only these comma-separated task IDs (e.g. `-task fix-add-bug`); unknown IDs fail with the available list. |
| `-attempts` | `1` | Suite and diff modes: retry a task until an attempt passes, up to N; enables the `Pass@≤N` KPI, and TTCS charges a retried solve with its failed attempts' wall. |
| `-bin` | `reasonix` | Path to the reasonix binary. |
| `-profile` | `baseline` | Tool-surface/runtime tier: `baseline` \| `economy` \| `balanced` \| `delivery`. All but `baseline` append `--profile <tier>` to the agent invocation; `baseline` passes no flag (byte-identical legacy control, behaviorally `balanced`). Economy starts with the core tool set and pays `connect_tool_source` rounds plus prefix resets to grow it — the report's Tool surface line prices that trade. |
| `-json` | *(none)* | Write the JSON report here (optional). |
| `-trajectories` | *(none)* | Suite mode: write one `<task-id>.trajectory.jsonl` per task into this directory (the agent's full event stream with timestamps — see `reasonix run --trajectory`). The report gains a time-attribution line (tools vs. model) and each JSON result a `trajectory` digest. |
| `-force-planner` | `false` | Suite mode: prefix each prompt with a plan-first directive so the two-model turn engages regardless of the planner gate. Use for the "with planner" arm of an A/B; results carry `plan_forced` so arms are only comparable with equal forcing. |
| `-anchor` | `blind` | Suite mode: which hypothesis the agent holds before it looks at anything — `blind` (none, the control) \| `correct` \| `wrong`. The seeded arms prefix each prompt with the task's authored seed and **skip** tasks that have none, so an unseeded control run never lands in a seeded denominator. Results carry `anchor`. See [Anchor resistance](#anchor-resistance). |
| `-cache` | `cold` | Suite mode: `cold` runs each task as a fresh session (the fair cross-agent comparison arm); `warm` primes the provider prefix cache with a one-step run in the same workdir first, measuring the long-lived-session steady state. Never mix arms in one report — compare them with `-mode compare cold.json warm.json`. |
| `-budget` | `800000` | Abort once total tokens cross this (`0` = no cap). Remaining tasks are reported as skipped. |
| `-meter` | *(off)* | Suite mode: route the benchmarked provider through the neutral measuring proxy, using this `config.toml` as the source. Spend is then counted at the request boundary instead of trusted from the harness. See [Neutral metering](#neutral-metering). |
| `-faults` | *(none)* | Suite mode: inject provider failures through the meter — absolute indices (`3:429`) and/or a cadence that scales with the run (`every:5:500`). Requires `-meter`. See [Fault recovery](#fault-recovery). |
| `-segments` | `1` | Suite mode: split each task into N resumed legs (`--continue` between them). The step budget is **divided**, never multiplied. See [Segmented runs](#segmented-runs). |
| `-steer` | *(none)* | Suite mode: deliver a user turn at a leg boundary, e.g. `"also handle empty input@2"`. Requires `-segments` to reach that leg. |
Diff-mode flags:
| Flag | Default | Purpose |
| --- | --- | --- |
| `-repo` | `.` | Repo root (diff mode). |
| `-base` | *(none)* | Base ref to diff the PR head against (diff mode). |
| `-test-cmd` | `go test` | Grader command run on the affected packages (diff mode). |
| `-max-steps` | `80` | Agent tool-call cap for the diff task. |
ch<-provider.Chunk{Type:provider.ChunkError,Err:fmt.Errorf("this model's maximum context length is %d tokens, however you requested %d tokens",p.window,in)}
t.Errorf("gen %d failed to fold: %s",r.Gen+1,r.Error)
}
ifr.LargestCall+reserve>window{
t.Errorf("gen %d largest summarizer call = %d tokens est.; with %d reserved for the digest that overflows the %d window",r.Gen+1,r.LargestCall,reserve,window)
}
ifr.SummarizerCalls>maxCalls{
t.Errorf("gen %d cost %d summarizer calls, over the %d ceiling",r.Gen+1,r.SummarizerCalls,maxCalls)
// probeAnswerContract rides with every probe question. A context full of tool
// calls invites the model to answer with another one, which scores as a lost
// fact when it is really a harness artifact — both arms get the same nudge.
constprobeAnswerContract="Answer using only the conversation above. Reply with the answer itself in plain text: no tool calls, no tool-call syntax, no explanation."
// noAnswerMarker and toolCallMarker label a reply that never answered, so the
// report can separate what compaction lost from what the harness failed to ask.
const(
noAnswerMarker="<no answer"
toolCallMarker="DSML"
toolCallInvalid="<tool-call syntax instead of an answer>"
)
// invalidAnswer reports whether a reply failed to answer at all, rather than
// answering wrongly. These are excluded from the survival rate and counted.
prompt="config.py silently accepts broken config files and the app then crashes much later somewhere else. Make loading fail fast with clear errors instead."
prompt="parse_all in parser.py only handles some of the files under fixtures/. Users need it to handle all of them. Extend it so every fixture is parsed."
prompt="Exactly one setting differs across config/base.toml, config/staging.toml and config/production.toml. Write AUDIT.md naming the setting that drifted, the environment it drifted in, and both values. Do not change any config."
"""Price after an integer-percent discount, in cents, rounded down."""
returnprice_cents-price_cents*(percent//100)
Loaded 100 of 7629 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.