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.
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 status bar could show a USD session cost beside a CNY wallet balance after the user selected USD pricing.
Root cause:
Balance rendering always preferred CNY and neither the local nor Remote Workbench balance query carried the effective Desktop pricing currency.
Fix:
Select the provider-reported balance that matches the effective Settings currency. When the provider only reports another currency, retain that real amount with an explicit ISO prefix instead of applying an implicit exchange rate. Carry the preference through an optional Remote Workbench request field while preserving the legacy CNY-first fallback for older callers.
Verification:
- go test ./... -count=1
- cd desktop && go test ./... -count=1
- pnpm typecheck && pnpm test:typecheck && pnpm test:remote
- pnpm exec tsx src/__tests__/use-controller-live-context.test.tsx
- pnpm build
- go run ./cmd/remote-protocol-gen -check -root .
- ./scripts/cache-guard.sh
Contract: A popover that renders before its anchor must leave the visible fallback and compute an anchored position without requiring a scroll or resize event.
Coverage: Consume the first scheduled measurement while the anchor is absent, then mount the anchor and assert that bounded frame retries recover the correct position and ready state.
Verification: Anchored popover regression test (9/9), frontend typecheck, and git diff --check.
Co-authored-by: SivanCola <[email protected]>
Problem: The defensive popover fallback could remain at the viewport edge when the first animation-frame measurement ran before its anchor mounted.
Root cause: A missing anchor ended the only guaranteed follow-up measurement, while assigning a ref later does not rerun the positioning effect.
Fix: Retry the initial measurement for at most eight animation frames, then stop. Preserve the last valid position after a successful measurement and clear the tracking ref when the popover closes.
Verification: Anchored popover regression test (9/9), frontend typecheck, and git diff --check.
Co-authored-by: SivanCola <[email protected]>
Integration: Bring the popover fallback fix onto the latest main-v2 without rewriting contributor history.
Verification: Focused popover tests, frontend typecheck, full frontend tests, and the production build follow in subsequent validation.
Contract: Anchored popovers stay visible before their first real measurement, become ready only after anchored geometry is available, and retain the last valid position during temporary anchor loss.
Coverage: Extend the existing real-component scroll test with deterministic unavailable-to-ready and ready-to-unavailable interleavings.
Verification: The focused anchored-popover regression passed, TypeScript typechecking passed, and git diff --check passed.
Co-authored-by: SivanCola <[email protected]>
Integration:
- Update PR #7123 to main-v2 at 84d3d1ae00.
- Preserve the macOS-only icon safe area and pre-signing replacement unchanged.
Verification:
- go test ./...
- (cd desktop && go test ./...)
- (cd desktop && go test -race .)
- bash -n scripts/desktop-build.sh
- git diff --check
Problem: Popovers without an initial measured position were hidden off-screen, while the first PR revision fabricated zero-sized rectangles and could replace a previously valid position with a top-left fallback.
Root cause: Measurement readiness and fallback visibility were represented by the same position state.
Fix: Keep the position null until real elements can be measured, preserve the last valid position during temporary anchor loss, and render an unmeasured popover at the visible edge without marking it ready.
Verification: The focused anchored-popover regression passed, TypeScript typechecking passed, and git diff --check passed.
Co-authored-by: SivanCola <[email protected]>
Integration: Bring the contributor branch onto the latest main-v2 without rewriting its published history.
Verification: The merge completed without conflicts; full frontend verification follows.
Problem:
The scoped footer overflow fix restores composer menus in the normal layout, but a terminal drawer or tall composer can still push a fixed-height menu above the viewport.
Root cause:
The menu capped itself with 50vh instead of the actual space above its composer anchor, and sibling-driven layout changes can move that anchor without resizing it.
Fix:
Preserve decision-only footer scrolling, measure the real anchor space while a menu is open, observe surrounding layout and viewport changes, and cap the menu at the smaller of 360px and that available height. Add DOM geometry coverage plus the scoped-footer guard from the overlapping test PR.
Verification:
- pnpm test
- pnpm build
- pnpm typecheck && pnpm test:typecheck
- go test ./... (desktop module)
- wails build -s -tags webkit2_41
- Browser geometry at 1280x720 with the terminal drawer and a tall composer
Fixes#7127
Co-authored-by: SivanCola <[email protected]>
Problem: The first stable CLI publication failed before GoReleaser could build because the protected release-control checkout appeared as an untracked directory in the immutable product checkout.
Root cause: Preview recovery reused an existing CLI release and never exercised GoReleaser dirty-state validation. A new stable release does, and the nested control-plane checkout made the otherwise clean product tree dirty.
Fix: Add only the exact release-control directory to the product checkout local exclude file, verify the nested checkout is ignored, and fail closed if any other product-tree dirt remains. Resolve the common Git directory absolutely so normal clones and linked worktrees behave consistently.
Verification:
- bash scripts/release-workflows.test.sh
- actionlint v1.7.7 release workflows
- bash scripts/cache-guard.sh
- bash -n scripts/release-workflows.test.sh
- git diff --check
Problem: Reconciliation treated a nil exact-cancel result as proof that the pending marker was removed, but the existing cancel API intentionally returns nil when a different target version or creation time is observed.
Root cause: The new reconciler relied on the transition return value without re-checking its public postcondition after releasing the transaction locks.
Fix: Re-read pending-update.json after cancellation, report success only when the marker is absent, and fail closed when the same or a newer transaction remains. The deterministic rewrite test now swaps in a different target version to exercise the inert-success path.
Verification: focused internal/repair and desktop updater/startup/handoff tests passed with and without the race detector; git diff --check passed.
Problem: Desktop updates could leave pending-update.json behind when the macOS handoff helper failed after the transaction was prepared. Later installs then failed permanently with a pending update already exists error.
Root cause: Startup and install entry points did not reconcile abandoned immutable update transactions, and the macOS parent treated a successful process spawn as a completed handoff without confirming that the child had validated the transaction.
Fix: Reconcile prepared or partially published transactions through exact cancel/rollback paths before every startup and install mode, preserve genuinely probationary releases with content-bound evidence, add a ready/proceed macOS handoff handshake, and surface recovery progress in both updater views.
Verification: go test ./...; cd desktop && go test ./...; focused repair and desktop go test -race suites; frontend updater tests, typecheck, CSS checks, production build, and bundle budgets.
Problem:\nPreview recovery rejected the existing public pointer even though its assets use the official immutable version directory.\n\nRoot cause:\nThe fallback validator replaced every legacy Preview asset base with the historical rolling desktop-preview directory. This made version-bound legacy manifests with a null release notes URL impossible to validate.\n\nFix:\nTry the immutable legacy layout first, then retain the rolling directory as a second Preview-only compatibility fallback. Add workflow contract coverage and a null release-notes manifest regression.\n\nVerification:\n- bash scripts/release-workflows.test.sh\n- actionlint v1.7.7 release workflows\n- bash scripts/cache-guard.sh\n- git diff --check
Problem:
Desktop Preview recovery compared a rebuilt manifest with the immutable published manifest before adopting the validated published record.
Root cause:
The directory comparison ran before the existing manifest was validated against its payload sizes and digests and copied into the recovery candidate.
Fix:
Validate and adopt the immutable manifest before comparing directories, and lock the ordering with static and behavioral regression coverage.
Verification:
- bash scripts/release-workflows.test.sh
- actionlint for release-desktop.yml
- bash scripts/cache-guard.sh
- bash syntax and git diff --check
AnchoredPopover's position state starts as null. In some WebKit (Wails)
render timings getBoundingClientRect() returns nothing yet, and the old
'if (!anchor || !menu) return' bailout left position unset forever:
the popover stayed visibility:hidden and parked off-screen at -9999
until a scroll/resize event happened to recompute it - so it only
appeared after the mouse moved over it.
Fall back to a zero-size rect so position is always computed (clamped
to a visible in-viewport spot), and use EDGE_GAP instead of -9999 as
the pre-layout fallback so an unmeasured popover is never hidden.
Problem: Rebuilding an immutable Desktop candidate produces byte-distinct minisign files, so recovery rejects an otherwise identical published directory.
Root cause: The directory verifier treated nondeterministic detached signatures as reproducible payload bytes.
Fix: Verify candidate and published signatures with the embedded public key, allow only verified signature-byte differences, and copy existing signatures into the recovery set so immutable objects are preserved.
Verification: Release workflow contracts and Actionlint pass; public Preview Linux and Windows installer assets verify against the embedded key.
Problem: Historical Preview recovery checks out the immutable product candidate, so recovery scripts added after that tag are absent.
Root cause: The CLI publisher mixed candidate source and publication control policy in one checkout, unlike the Desktop publisher.
Fix: Add a protected workflow-SHA control checkout and run the CLI publication decision from it while keeping product builds pinned to the approved candidate.
Verification: release-workflows.test.sh and Actionlint pass; regression assertions reject candidate-controlled publication policy.
Problem:
A failed Preview publisher cannot consume a workflow fix by rerunning the original Actions attempt, while the immutable Preview tag must not be moved to the repaired main-v2 commit.
Root cause:
The documented all-surface Preview recovery path still required the Preview tag to equal current main-v2, making recovery of an ancestor candidate impossible.
Fix:
Add an opt-in Preview recovery input, keep the control workflow pinned to protected current main-v2, allow only a historical Preview tag that remains on main-v2 history, and propagate the recovery authorization to the CLI publisher.
Verification:
- bash scripts/release-workflows.test.sh
- bash -n scripts/resolve-preview-release.sh scripts/release-workflows.test.sh
- git diff --check
Problem:
Orchestrated Desktop releases passed an empty direct tag input to the R2 mirror revalidation step, blocking Preview publication and the next Stable mirror.
Root cause:
The mirror job used inputs.tag even though Preview and Stable orchestrators authorize releases through inputs.approved_cli_tag.
Fix:
Revalidate the orchestrator-approved CLI tag in the mirror job and lock the mapping with a workflow contract regression assertion.
Verification:
- bash scripts/release-workflows.test.sh
- git diff --check
Problem:
- The first 1.19.0 Preview tag exposed a Desktop orchestrator authorization failure before any public surface could complete.
- Recovery must use a new immutable Preview ordinal instead of moving the published tag.
Fix:
- Add the reviewed bilingual 1.19.0-preview.2 record for the release control-plane repair in PR #7118.
- Bind CLI, Desktop, and npm identities to Preview ordinal 2.
Verification:
- node scripts/release-notes.mjs validate
- node --test scripts/release-notes.test.mjs
- git diff --check
- Release-record privacy scan
Problem:
- The protected Preview orchestrator passed orchestrated=true to the Desktop release workflow.
- The immutable candidate resolver treated every orchestrated call as Stable and rejected every Desktop Preview before signing preflight.
Root cause:
- release-desktop.yml did not forward the trusted orchestrator kind to the resolver.
- The resolver had a hard-coded Stable-only check, and contract tests covered only standalone Preview resolution.
Fix:
- Forward and validate the stable/preview orchestrator identity in every candidate resolution step.
- Permit the Preview orchestrator to authorize its approved SHA, including recovery after main-v2 advances, while retaining ancestry and tag checks.
- Add success, mismatch, and recovery regression coverage.
Verification:
- bash scripts/release-workflows.test.sh
- git diff --check
- Privacy scan of the patch
Problem:
- The v1.19.0 stable release needs a reviewed catalog record before stable tags can be pushed.
- The hosted generator returned empty content, and the live fallback produced a schema-invalid change body twice.
Fix:
- Derive the stable record deterministically from the reviewed preview record because v1.18.0 and v1.18.0-preview.2 share the same source SHA.
- Replace only the stable channel wording, prior version, release links, and CLI/Desktop/npm build identities.
Verification:
- node scripts/release-notes.mjs validate
- node --test scripts/release-notes.test.mjs
- git diff --check
- Privacy scan of the release record diff
Problem:
- The v1.19.0-preview.1 release needs an exact reviewed catalog record before tagging.
- The hosted notes generator returned empty content twice.
Fix:
- Generate a bilingual prerelease record from the first-parent range v1.18.0-preview.2..main-v2 using the repository script and a validated DeepSeek model.
- Bind CLI, Desktop, and npm build identities to preview ordinal 1.
Verification:
- node scripts/release-notes.mjs validate
- node --test scripts/release-notes.test.mjs
- git diff --check
- Privacy scan of the release record diff
Problem:
The curated GLM Coding Plan Anthropic presets selected glm-5.2[1m] by default, which the live endpoint rejected with model-not-found for a valid Coding Plan credential.
Root cause:
The presets promoted a Claude Code-specific model alias to the generic Anthropic-compatible default instead of using the documented glm-5.2 model identifier.
Fix:
Default both China and global Anthropic-compatible Coding Plan presets to glm-5.2 while retaining the optional alias and the one-million-token context configuration. Keep browser mocks and localized descriptions aligned.
Verification:
- go test ./internal/config
- go test ./...
- pnpm --dir desktop/frontend test:all
- ./scripts/cache-guard.sh
- live GLM Coding Plan Anthropic request through the default provider selection
Problem:
PR #7103 conflicted with the merged Responses preset, retained raw non-tool reasoning across PostLLMCall hooks, and omitted the Anthropic preset from the Desktop browser/localization contract.
Root cause:
Both protocol presets added adjacent config tests independently; the Anthropic client advertised full reasoning round trips despite replaying only tool-call reasoning; frontend mock and i18n tables were not extended with the backend preset.
Fix:
Merge current main-v2 while retaining both protocol tests, scope raw reasoning retention to tool-call turns, and add the localized Desktop mock, adjacent preset ordering, and regression coverage.
Verification:
- go test -p 1 ./... -count=1
- cd desktop && go test -p 1 ./... -count=1
- go test -race ./internal/provider/anthropic ./internal/agent ./internal/config ./internal/boot -count=1
- go vet ./... (root and desktop)
- pnpm --dir desktop/frontend test:all
- pnpm --dir desktop/frontend build
- ./scripts/cache-guard.sh
Integration:
Bring PR #7094 onto current main-v2 after the Responses API merge. Preserve both the new Responses provider-kind surface and the provider model-cache/race fixes.
Verification:
Passed root and Desktop tests, vet, and build; frontend full tests, typecheck, and production build; focused Go race and frontend cache/privacy regressions; cache-impact guard; and git diff --check.
Problem:
Markdown identifiers were treated as commands solely because they were backticked, while acknowledgement phrases could suppress a real follow-up task.
Root cause:
Inline shell parsing accepted every non-empty word, and the task heuristic returned immediately on acknowledgement wording without inspecting surrounding clauses.
Fix:
Require command-shaped tokens independent of Markdown quoting, and preserve tasks before acknowledgements or after explicit transition boundaries. Add focused and end-to-end regressions for both cases.
Verification:
- go test ./internal/agent -count=1
- go test ./... -count=1
- go test -race ./internal/agent -count=1
- go vet ./...
- cd desktop && go test ./... -count=1
- ./scripts/cache-guard.sh
- scripts/check-cache-impact.sh with the live PR body
- git diff --check
Integration:
Bring PR #7094 onto the current main-v2 baseline after the Remote Workbench CLI bootstrap merge. The base changes do not overlap provider settings, credential locking, or model-cache files.
Verification:
The combined tree will be rechecked with Root and Desktop tests, focused frontend regressions, cache guard, and git diff validation before push.
Problem:
Credential rotation could interleave with provider catalog compare-and-apply, frontend caches ignored the opaque credential revision, and auto-refresh cooldown keys persisted provider header values.
Root cause:
Credential writes and catalog saves used different lock domains, the cache identity omitted modelCatalogFingerprint, and SettingsPanel serialized the full provider request identity into sessionStorage.
Fix:
Serialize credential-store writes with a shared process and advisory file lock, revalidate a content-derived credential revision before catalog commit, key caches and cooldowns by the opaque fingerprint, and add deterministic interleaving and privacy regressions.
Verification:
Focused Go race tests, root and Desktop test suites, Go vet/build, frontend test/typecheck/build, cache guard, and git diff --check passed.
Co-authored-by: SivanCola <[email protected]>
Problem:
- The DeepSeek Responses preset fell back to an English description in Chinese settings and appeared far below the official DeepSeek entry.
- Browser preview data omitted the preset, so local UI checks did not match production.
Root cause:
- The preset had no frontend localization mapping or locale entries.
- Production and mock preset ranking did not give the DeepSeek Responses entry an explicit priority.
Fix:
- Localize the preset description in English, Simplified Chinese, and Traditional Chinese.
- Rank it first among curated presets, making it second overall after DeepSeek Official.
- Mirror the production preset and order in browser mock data with regression coverage.
Verification:
- go test ./...
- cd desktop && go test ./...
- pnpm test
- pnpm test:typecheck
- pnpm build
- ./scripts/cache-guard.sh
- Browser preview verified card order and Chinese copy
Co-authored-by: SivanCola <[email protected]>
Problem:
DeepSeek V4 Flash lost its supported low effort on the Anthropic endpoint, legacy effort aliases used the Pro mapping for every model, and caller temperature was silently dropped.
Root cause:
The compatibility path normalized effort without considering DeepSeek's model routing and reused the native Anthropic request shape that intentionally omits sampling controls.
Fix:
Apply Flash/Pro-aware effort normalization including Claude aliases and fallback routing, expose Flash low effort in the preset, preserve temperature only for official DeepSeek Anthropic requests, and update documentation and regression coverage.
Verification:
- go test ./internal/provider/anthropic ./internal/config ./internal/boot -count=1
- go test -race ./internal/provider/anthropic ./internal/config ./internal/boot -count=1
- ./scripts/cache-guard.sh
- go vet ./...
- go test -p 1 ./... -count=1
- cd desktop && go test -p 1 ./... -count=1
Problem:
Delivery advisory classification still missed affirmative mutations after negated clauses, treated remote diagnostic verbs as observable work, and allowed ordinary commands to escape the evidence gate.
Root cause:
Negation used a fixed prefix window, advisory verbs overrode question semantics globally, and command detection relied on a narrow exact-string list that drifted from the task heuristic.
Fix:
Scope negation across directive boundaries, classify advisory language per clause, recognize concrete commands and file anchors without confusing prose or email addresses, and share mutation intent with the task heuristic. Add a bilingual classification matrix and end-to-end readiness regressions.
Verification:
- go test ./internal/agent -count=1
- go test ./... -count=1
- go test -race ./internal/agent -count=1
- go vet ./...
- cd desktop && go test ./... -count=1
- ./scripts/cache-guard.sh
- scripts/check-cache-impact.sh with the live PR body
- git diff --check
Problem:
Desktop lint rejected obsolete SSH factory wrappers. Workbench CLI reuse also required SFTP before checking an exact PATH binary and could return a relative executable path.
Root cause:
The bootstrap initialized managed-storage dependencies before the zero-install reuse path, while probe validation checked only the Build ID. Compatibility wrappers remained after callers migrated to explicit-binary factories.
Fix:
Probe an exact absolute PATH binary before SFTP, defer platform detection until installation, reject non-absolute probe results, remove unused wrappers, and cover SFTP-disabled and relative-PATH regressions.
Verification:
go test -p 1 ./... -count=1
cd desktop && go test ./... -count=1
go test -race ./internal/remote/bootstrap -count=1
cd desktop && go test -race . -run Test\(EnsureWorkbenchCLI\|GoSSHStream\|RemoteSSHTransport\) -count=1
go vet ./... (root and desktop)
Windows desktop test package cross-compile
./scripts/cache-guard.sh
Problem: Remote Workbench on fresh or cross-platform SSH hosts could exit before initialize and surface only a generic connection-closed error.
Root cause: Desktop launched reasonix from the remote PATH without provisioning an exact protocol-compatible build, while the Go SSH path discarded stderr and session exit failures.
Fix: Probe the complete Workbench Build ID, securely provision a verified platform release into a build-specific remote path, launch that executable explicitly, and preserve classified bootstrap failures. Extend legacy serve bootstrap to support verified cross-platform release uploads.
Verification: go test ./...; cd desktop && go test ./...; frontend remote tests, typecheck, and production build; focused race tests and go vet; cache and protocol guards; Windows and Linux/arm64 cross-compilation.
Context: Synchronize PR #7103 with the latest main-v2 after applying the DeepSeek Anthropic reasoning replay review fixes.
Compatibility: Preserve both the existing DeepSeek Anthropic changes and the latest provider, agent, Desktop, and protocol updates from main-v2 without rewriting branch history.
Verification: The merge completed without conflicts and git diff --cached --check passed; full root, Desktop, race, and cache validation follows on the combined tree.
Problem: Historical DeepSeek Anthropic tool-call reasoning could be omitted when a later request no longer declared tools, while the reasoning guide could misstate the model-specific effort scales.
Root cause: Request serialization keyed reasoning replay to the current tool list and thinking mode instead of the historical assistant tool-call turn. The documentation also combined Flash and Pro capabilities.
Fix: Replay unsigned DeepSeek thinking blocks only for historical assistant tool-call turns regardless of the current tool declaration or thinking mode, add focused regression coverage, and document the separate Chat Completions and Anthropic effort scales.
Verification: go test ./internal/provider/anthropic -run TestBuildRequestDeepSeek -count=1 -v; go test ./internal/config -run TestDeepSeek\|TestNormalizeEffortDeepSeek -count=1; git diff --check.
Problem:
Stateful continuations dropped system instructions, and the DeepSeek Responses preset exposed a provider-wide catalog containing unsupported models.
Root cause:
The continuation path returned before instructions were serialized, while the preset reused a models endpoint broader than Responses API support.
Fix:
Serialize instructions before choosing full-history or previous-response continuation, keep DeepSeek Responses on its curated Flash-only list, and add regressions for both contracts.
Verification:
- go test ./internal/provider/responses ./internal/config -count=1
- go test -race ./internal/provider/responses ./internal/config -count=1
- ./scripts/cache-guard.sh
- go test ./...
- go vet ./...
- cd desktop && go test ./...
- cd desktop && go vet ./...
Co-authored-by: SivanCola <[email protected]>
Problem:
- CodeQL treated the unkeyed SHA-256 catalog fingerprint as weak hashing of sensitive credential-related data.
Root cause:
- The digest crossed the Wails boundary without a process-private key, even though it contained only provider identity and credential-store metadata.
Fix:
- Generate a process-private random key and compute the opaque catalog token with HMAC-SHA256.
Verification:
- focused provider catalog tests under go test -race
- go test ./... and go build ./... in desktop
- git diff --check
Problem:
- A delayed background model refresh could submit a full ProviderView after a newer manual save and silently restore old provider settings.
- Switching settings tabs or rotating a credential did not invalidate the pending write.
Root cause:
- The background effect had no request generation guard.
- SaveProviders serialized writes but had no compare-and-apply contract, so an old snapshot could still win last.
Fix:
- Replace full-provider batch writes with narrow model-catalog updates guarded by an opaque provider and credential fingerprint.
- Compare and apply updates while holding the config edit lock, skipping stale snapshots and rebuilding only after real changes.
- Invalidate outdated frontend completions with an effect generation guard.
- Add deterministic stale-completion, credential-rotation, atomicity, and tab-switch regression coverage.
Verification:
- go test -race focused desktop catalog tests
- go test ./... and go build ./... in desktop
- pnpm test and pnpm build in desktop/frontend
- go test ./...
- ./scripts/cache-guard.sh
- git diff --check
Context: bring PR #7094 onto the current integration baseline before applying the stale-write remediation.
Verification: merge completed without conflicts.
Resolve the overlapping OpenAI provider changes by preserving both the DeepSeek Beta prefix continuation path and the merged Gemini thought-signature/model-normalization behavior.\n\nVerification:\n- go test ./internal/provider/openai ./internal/agent -count=1\n- go test -race ./internal/provider/openai -count=1\n- GOWORK=off go vet ./internal/provider/openai\n- ./scripts/cache-guard.sh\n- go test ./... -count=1\n- cd desktop && go test ./... -count=1\n- git diff --cached --check
Problem:
The ACP sandbox degradation test failed on Windows even though the runtime
override path behaved correctly.
Root cause:
Windows intentionally normalizes a config-file `bash = "enforce"` setting to
`off`, so the test never entered the unavailable enforced-sandbox branch it
intended to exercise.
Fix:
Set the effective `enforce` override explicitly in both the degradable and
fail-closed test cases, matching the supervisor command-line path on every OS.
Verification:
- go test ./internal/cli -run 'TestACPSupervisorRuntimeState' -count=1
- go test -race ./internal/cli -run 'TestACPSupervisorRuntimeState' -count=1
- GOOS=windows CGO_ENABLED=0 go test -c ./internal/cli
- go test ./...
- cd desktop && go test ./...
Co-authored-by: SivanCola <[email protected]>
Problem: CI rejected the advisory classifier with staticcheck S1008 after the functional fix was pushed.
Root cause: The final boolean branch returned true or false explicitly instead of returning the computed negated-mutation value.
Fix: Return negatedMutation directly without changing classifier behavior.
Verification: go test ./internal/agent -count=1; go vet ./...; git diff --check.
Co-authored-by: SivanCola <[email protected]>
Problem:
DeepSeek thinking-mode responses can exhaust max_tokens entirely in reasoning_content before emitting visible text, so the prefix-continuation wrapper stopped without attempting the documented Beta recovery.
Root cause:
The continuation gate required accumulated visible text even when a valid thinking-mode reasoning prefix was available.
Fix:
Allow one bounded Beta continuation when thinking mode has accumulated reasoning_content, including an empty assistant content prefix. Add an SSE regression that verifies the wire payload, endpoint sequence, merged usage, and final response.
Verification:
- go test ./internal/provider/openai ./internal/agent -count=1
- go test -race ./internal/provider/openai -count=1
- GOWORK=off go vet ./internal/provider/openai
- ./scripts/cache-guard.sh
- git diff --check
Problem: Common negated install requests and remote application run questions still armed delivery mutation or host-evidence gates, preventing advisory responses from completing.
Root cause: Mutation negation depended on a small list of complete phrases, while generic run/test keywords were treated as host-observable without repository, file, or command context.
Fix: Classify English and Chinese negation around each mutation verb, preserve affirmative mixed-intent clauses, and require concrete anchors before run/test wording overrides advisory questions. Add regressions for the reported install and WPS cases plus local technical-work controls.
Verification: go test ./...; cd desktop && go test ./...; go vet ./...; ./scripts/cache-guard.sh; git diff --check.
Co-authored-by: SivanCola <[email protected]>
Problem:
Persisted ACP status could restore `running` after a process restart even
though the new runtime owned no active turn, leaving supervisors waiting on
ghost work.
Root cause:
`restoreStatusTelemetry` preserved the process-local running state instead of
converting an interrupted snapshot into a new terminal recovery observation.
Fix:
Always restore sessions idle. When the saved snapshot was running, advance the
status sequence and publish `recovery_paused` with an interrupted paused
outcome while preserving partial usage.
Verification:
- go test ./internal/acp ./internal/cli ./internal/control ./internal/boot
- go test -race ./internal/acp ./internal/cli ./internal/control ./internal/boot
- ./scripts/cache-guard.sh
- go test ./...
- cd desktop && go test ./...
Co-authored-by: SivanCola <[email protected]>
Problem: Reasonix treated the official DeepSeek Messages endpoint as a generic Anthropic-compatible service, which misses its unsigned thinking replay, text-only input, and automatic cache contracts.
Fix: keep Chat Completions as the default, add an optional curated Anthropic preset, and scope endpoint-specific reasoning, effort, cache, and vision behavior to official DeepSeek hosts.
Verification: focused provider/config/boot tests, Desktop tests, serial root tests, go vet, cache guard, and diff checks pass. The opt-in live probe reached the endpoint but the inherited local credential returned HTTP 401.
Problem:
CI staticcheck reported a redundant break at the end of a switch case in the Responses SSE loop.
Root cause:
The terminal-event case already exits the switch naturally before the outer terminal guard runs.
Fix:
Remove the redundant break without changing stream behavior.
Verification:
go test ./internal/provider/responses
go vet ./internal/provider/responses
git diff --check
Problem:
Reasonix could not use DeepSeek V4 Flash through the official stateless Responses API, while the contributor branch mixed protocol work with unrelated artifacts and missed tool/reasoning round-trip correctness.
Root cause:
The initial implementation treated Responses events and previous_response_id too loosely, dropped assistant reasoning/content on tool turns, duplicated terminal text, and did not persist context-mode configuration.
Fix:
Add a focused Responses provider with deterministic full-history serialization, DeepSeek reasoning/tool replay, safe stateful-prefix validation and stale-ID fallback, vendor-scoped DashScope headers, terminal-state handling, config round-tripping, and a DeepSeek Flash preset.
Verification:
go test ./internal/provider/responses ./internal/config ./internal/boot
go test -race ./internal/provider/responses ./internal/config ./internal/boot
go test ./...
(cd desktop && go test ./...)
go vet ./...
(cd desktop && go vet ./...)
./scripts/cache-guard.sh
Co-authored-by: clearnature <[email protected]>
Integration:
- Include the terminal drawer layout work that landed during remediation validation.
- Preserve the model polling fixes on the current review base.
Verification:
- Merge completed without conflicts; targeted frontend and Desktop build checks follow.
Integration:
- Include the main-v2 release-channel and DeepSeek effort updates that landed while PR checks were running.
- Keep the remediated model polling changes tested against the current base.
Verification:
- Merge completed without conflicts; post-merge Desktop and frontend validation follows.
Problem:
The supervisor status extension could block default ACP startup when an OS sandbox backend was unavailable, report stale planner state after session rebuilds, expose non-standard ACP fields, and publish unsanitized status text.
Root cause:
Runtime policy was captured only at session creation, sandbox availability was treated as a startup requirement even for automatic policy, permission reasons were added at the ACP v1 root, and presentation text was reused as public status data.
Fix:
Recompute runtime state from the selected model and profile after rebuilds, distinguish explicit sandbox enforcement from automatic degradation, publish truthful sandbox availability, keep extension data under _meta, and normalize/redact stable status fields.
Verification:
- go test ./internal/acp ./internal/cli
- go test -race ./internal/acp ./internal/cli
- ./scripts/cache-guard.sh
- go test ./...
- cd desktop && go test ./...
Co-authored-by: SivanCola <[email protected]>
Problem:
DeepSeek V4 Flash xhigh selections followed the generic DeepSeek mapping to max instead of the documented high effort.
Root cause:
The model capability registry described selectable levels but had no model-specific compatibility aliases.
Fix:
Add a Flash-specific xhigh-to-high alias and regression coverage for inferred and explicit DeepSeek protocols while preserving Pro xhigh-to-max behavior.
Verification:
- go vet ./...
- go test ./... -count=1
- cd desktop && go test ./... -count=1
- cd desktop/frontend && pnpm build
- ./scripts/check-cache-impact.sh
- ./scripts/cache-guard.sh
Problem:
- Settings writes returned before controller rebuilds and broke deferred lease retry.
- Failed model fetches never retried, cache identities collided, and credential changes kept stale entries.
- Background catalog updates rebuilt once per provider.
Root cause:
- The shared config apply path was made fire-and-forget.
- Backoff tracked only a delay without a deadline, and the key used modelsURL instead of modelsUrl.
- Auto-refresh persisted providers individually.
Fix:
- Restore synchronous failure-atomic rebuilds and deferred lease handling.
- Add typed time-based cache identity, backoff, invalidation, and stale-inflight guards.
- Batch provider catalog persistence into one transaction and runtime rebuild.
- Key refresh cooldowns by provider set and omit failed batch entries without JSON nulls.
Verification:
- go test ./... (desktop)
- pnpm test
- pnpm build
- pnpm typecheck
- ./scripts/cache-guard.sh
- git diff --check
Problem:
Gemini-authored tool calls keep opaque thought signatures in session history. After a same-session switch to another OpenAI-compatible provider, the generic request builder forwarded Gemini-only extra_content.google fields to that provider.
Root cause:
Request serialization checked only whether a stored signature existed; it did not check whether the current endpoint or model spoke Gemini thought-signature semantics.
Fix:
Detect the official Gemini endpoint and Gemini model IDs used through compatible gateways. Emit thought signatures only for those current clients, preserve them in provider-neutral history, and cover official, gateway, and switched-provider request shapes.
Verification:
- go test ./internal/provider/...
- ./scripts/cache-guard.sh
- git diff --check
Co-authored-by: SivanCola <[email protected]>
Co-authored-by: dailingtao <[email protected]>
Integration:
- Bring the contributor branch onto the current main-v2 baseline.
- Preserve the model polling performance changes for review remediation.
Verification:
- Git completed the semantic merge without conflicts.
Problem: Advisory diagnostics need to finish without synthetic work, but the initial fix also let read-only technical tasks bypass host-observable evidence and let negated clauses mask later mutation requests.
Root cause: The evidence gate was reduced to the mutation classifier, and mutation negation returned for the whole input instead of the clause it qualified.
Fix: Keep advisory, evidence, and mutation decisions separate; prefer concrete review, test, repository, and file signals; and classify mutation negation per clause. Add classifier and end-to-end readiness regressions for advisory, read-only, and mixed-intent prompts.
Verification: go test ./internal/agent -count=1; go vet ./...; go test ./internal/tool/builtin/ ./internal/boot/ -count=1; git diff --check.
Co-authored-by: SivanCola <[email protected]>
Problem:
Narrow workbench windows placed the terminal drawer over the status bar, and persisted terminal heights could collapse chat after the window shrank. The hidden resize separator also remained in the keyboard focus order.
Root cause:
Responsive CSS kept the drawer and separator on their wide-layout grid coordinates, while the rendered height reused the stored preference without clamping it to the current viewport.
Fix:
Keep the responsive drawer and separator in the content column, clamp the rendered height whenever the viewport changes, add keyboard resizing, and remove the closed separator from accessibility navigation. Add focused regression contracts for each boundary.
Verification:
- pnpm test:terminal
- pnpm typecheck
- pnpm check:css
- app chrome, status bar, and layout default contract tests
- browser checks at 800x700 and 1280x480
Problem:
DeepSeek V4 Flash supports low, high, and max reasoning effort, but Reasonix exposed only high and max and normalized low to high.
Root cause:
DeepSeek effort capabilities and provider validation treated depth support as protocol-wide instead of preserving model-specific levels.
Fix:
Add low to the V4 Flash capability, preserve existing Pro and generic DeepSeek behavior, forward low only for V4 Flash or explicitly configured providers, and update desktop guidance.
Verification:
- go test ./internal/config ./internal/provider/openai -count=1
- cd desktop && go test ./... -count=1
- pnpm typecheck
- pnpm build
- ./scripts/check-cache-impact.sh
- ./scripts/cache-guard.sh
Construct the disabled-thinking test client directly so go vet does not flag a copy of the embedded atomic authentication state.
Verification:
- go vet ./...
- go test ./internal/provider/openai -count=1
Problem:
DeepSeek text responses that reached the output limit stopped at finish_reason=length even though the official API supports assistant-prefix continuation.
Root cause:
The OpenAI-compatible provider treated every completed stream as terminal and had no bounded path to the DeepSeek Beta chat endpoint.
Fix:
Continue official DeepSeek text replies once with a wire-only assistant prefix, preserve reasoning_content in thinking mode, merge usage across requests, and fall back to the original truncated answer when Beta continuation is unavailable. Keep ordinary request bytes, custom gateways, tool calls, and persisted history unchanged.
Verification:
- go test ./internal/provider/openai -count=1
- go test ./internal/agent ./internal/provider/openai -count=1
- ./scripts/check-cache-impact.sh
- ./scripts/cache-guard.sh
- go test ./... -count=1
- cd desktop && go test ./... -count=1
Problem:
An interrupted Stable or Preview event could publish some or all npm packages before another surface failed. Rerunning the documented orchestrator then failed on immutable versions that already existed, so postflight could not create the release marker.
Root cause:
npm/build.mjs always published every package directly under the public dist-tag and required that tag to equal the recovering version.
Fix:
Record the approved candidate SHA in every package, reuse only matching immutable versions, fill missing packages under a temporary tag, and advance public dist-tags monotonically after the full package set exists. Conflicting provenance fails closed and stale recovery cannot roll a newer channel backward.
Verification:
- node --test npm/publish.test.mjs
- bash scripts/release-workflows.test.sh
- actionlint release workflows
- npm six-platform dry build
- ./scripts/cache-guard.sh
Problem:
A TurnDone event could reach the frontend before an in-flight TrySteer rejection settled, leaving preserved guidance stranded on the shelf while the session was idle.
Root cause:
The idle queue effect skipped the item while its steer request was marked in flight, but clearing that marker after rejection did not change any effect dependency.
Fix:
Re-arm the idle queue only after a rejected steer settles, so an already-finished turn submits the preserved guidance as the next explicit user turn. Keep ordinary follow-up failures user-controlled to avoid automatic retry loops.
Verification:
The focused 228-case composer suite, frontend production and test typechecks, production build, and bundle budgets passed.
Problem:
Late guidance could be reported as accepted after its target turn stopped consuming input, Desktop profile previews could allow explicit Ask rules without an approver, and concurrent transcript durability lacked a real multi-session reload regression.
Root cause:
Steer intake closed after a separate queue check while host adapters used a void compatibility API, the preview used the legacy nil-approver headless gate, and existing concurrency coverage exercised only the display sidecar.
Fix:
Make final steer admission atomic, route observable hosts through TrySteer, retain rejected Desktop guidance as the next explicit turn, warn on abnormal unapplied guidance, preserve dynamic Bash policy while using an explicit fail-closed preview gate, and verify twelve concurrent Desktop sessions through disk reload.
Verification:
Passed focused race tests, root and Desktop Go suites, Go vet, frontend typechecks and focused tests, frontend production build and bundle budgets, and all eight cache-guard scenarios.
Problem:
- The settings-page subagent try path still used the permissive legacy headless gate.
- Permission provenance could name an Ask rule overridden by a session grant.
- npx snapshot-update equals forms were misclassified as verification, and headless migration docs were stale.
Root cause:
- One entrypoint bypassed the explicit headless posture builder.
- Rule provenance rescanned matching rules without checking the candidate decision.
- npx flag normalization and bilingual headless-mode documentation were incomplete.
Fix:
- Pin subagent try runs to fail-closed headless Ask and add an entrypoint regression.
- Validate rule provenance against the actual per-subject decision.
- Reject vitest/jest snapshot update equals forms and document --auto/-y migration without adding settings.
Verification:
- go test -p 1 ./... -count=1
- cd desktop && go test ./... -count=1
- affected package and focused race suites
- Node 22 frontend tests and typecheck
- scripts/cache-guard.sh and scripts/check-cache-impact.sh
- git diff --check
Problem:
A TurnDone event could reach the frontend before an in-flight TrySteer rejection settled, leaving preserved guidance stranded on the shelf while the session was idle.
Root cause:
The idle queue effect skipped the item while its steer request was marked in flight, but clearing that marker after rejection did not change any effect dependency.
Fix:
Re-arm the idle queue only after a rejected steer settles, so an already-finished turn submits the preserved guidance as the next explicit user turn. Keep ordinary follow-up failures user-controlled to avoid automatic retry loops.
Verification:
- pnpm exec tsx src/__tests__/composer-goal-toggle.test.tsx
- pnpm typecheck
- pnpm test:typecheck
- pnpm build
- go test ./... -count=1
- cd desktop && go test ./... -count=1
- focused root and Desktop go test -race coverage
- ./scripts/cache-guard.sh
Problem:
AST math classification rejected delimiter padding, Young macros copied their internal KaTeX expansion, and common SI-unit contexts left numeric formulas literal.
Root cause:
The policy classified the parser value without trimming, normalization kept only the rendered Young expansion, and numeric context used a narrow unit alternation.
Fix:
Separate authored and rendered math sources through the AST pipeline, classify a trimmed projection while preserving exact copy source, and recognize structured SI/scientific units.
Verification:
- pnpm exec tsx src/__tests__/math-golden.test.ts (263 passed)
- pnpm exec tsx src/__tests__/message-selection-copy.test.ts (13 passed)
- pnpm typecheck
- pnpm test:typecheck
- pnpm test
- pnpm build
- pnpm check:css
- go test ./...
- cd desktop && go test ./...
Problem:
- Preview events for different versions could overlap and let an older npm canary publish last.
- CLI, Desktop, and npm child workflows still exposed standalone public Preview paths that bypassed the all-surface release marker.
Root cause:
- The Preview concurrency key included the version tag, while npm had no channel-level serialization.
- Child workflow dispatch inputs and resolvers treated Preview publication as independently recoverable.
Fix:
- Serialize every Preview event and npm dist-tag publisher, and require public CLI, Desktop, and npm Preview releases to come from release-preview.yml.
- Keep Stable/RC recovery and non-publishing Desktop signing checks available, document unified Preview recovery, and add regression coverage.
Verification:
- bash scripts/release-workflows.test.sh
- actionlint v1.7.7 on all release workflows
- git diff --check
Apply performance fixes for model-list polling jank in Settings:
- F1: single-flight dedup + 60s TTL cache (providerModelCache.ts) around
FetchProviderModels; invalidate on key change.
- F2: exponential backoff 1s->60s for failed fetches; skip background
auto-refresh on saveData/slow-2g/2g connections.
- F3: startTransition + useDeferredValue around large model candidate
lists; content-visibility keeps 400+ model rows cheap.
- F4: 30s sessionStorage cooldown for auto-refresh keyed on provider set,
breaking the refreshKey <-> SaveProvider loop.
- F5: rebuild runs in a goroutine after config persist (async, non-blocking
binding response); emit settings:rebuild-{complete,failed} events so the
UI leaves the saving state on completion.
- F6: FetchAllProviderModels batches all providers in one round-trip
(errgroup, 4 concurrent, 15s per-provider timeout); SettingsPanel falls
back to per-provider cached calls on batch failure.
Adds golang.org/x/sync for errgroup (go.mod/go.sum).
When a user asks a diagnostic/troubleshooting question (e.g. "why
isn't my Zotero plugin working in WPS"), the delivery gate currently
demands host-observable work before the agent may finish — even though
the agent cannot run fixup commands on the user's machine. Because
deliveryTaskNeedsEvidence delegates to heuristicInputIsTask (whose
keyword set catches nearly every real-world prompt), the gate deems
almost every turn a task and blocks the final answer. The agent
retries up to maxFinalReadinessBlocksWithProgress (6) times, each
retry producing an identical diagnostic reply, until the turn is
failed — a dead-loop that wastes tokens and frustrates the user.
Root cause: deliveryTaskNeedsEvidence is not gated on whether the task
actually expects a mutation. Diagnostic/advisory conversations
naturally end when advice is given and should not require observable
work.
Fix: return false from deliveryTaskNeedsEvidence when
deliveryTaskNeedsMutation returns false. This keeps evidence
enforcement in place for mutation-worded tasks ("fix the crash in
a.go") while letting pure diagnostic conversations complete after the
agent has provided its advice.
Additional: add negation phrases to deliveryTaskNeedsMutation
("不敢重新安装", etc.) so that inputs containing mutation keywords in
an explicitly negative context are still classified as non-mutation.
Tests added:
- TestDeliveryTaskNeedsEvidenceSkipsDiagnosticConversations: verifies
that six diagnostic inputs (Chinese and English) are classified as
NOT needing evidence, and that mutation-worded inputs still do.
- TestDeliveryDiagnosticConversationCompletes: end-to-end test showing
a text-only advice turn (no tool calls) completes without a
FinalReadinessError when the task is diagnostic.
All 67 existing Delivery/FinalReadiness tests pass unchanged.
Problem:
The integration branch carried early permission and migrated-session variants that broadened PowerShell rule parsing, omitted prompt restoration for legacy Desktop sessions, and left public permission docs inconsistent with headless behavior and the dynamic Bash opt-in.
Root cause:
Compatibility handling lived in the generic rule parser, prompt-less migrated sessions returned before receiving the deterministic system prompt, and the integrated documentation did not include the final safety contracts.
Fix:
Scope legacy PowerShell expansion to additive deny rules, restore and persist fresh prompts for migrated Desktop sessions, exercise the real local-only steer format, align session expectations, and document fail-closed headless execution plus allow_dynamic_bash.
Verification:
- go test -p 1 ./...
- desktop go test -p 1 ./...
- focused race tests for permission, control, agent, capability, config, instruction, and Desktop session paths
- go vet for root and Desktop
- frontend tests and production build
- site tests and production build
- scripts/cache-guard.sh
Co-authored-by: ipanta <[email protected]>
Problem:
Late guidance could be reported as accepted after its target turn stopped
consuming input, Desktop profile previews could allow explicit Ask rules
without an approver, and concurrent transcript durability lacked a real
multi-session reload regression.
Root cause:
Steer intake closed after a separate queue check while host adapters used a
void compatibility API, the preview used the legacy nil-approver headless
gate, and existing concurrency coverage exercised only the display sidecar.
Fix:
Make final steer admission atomic, route observable hosts through TrySteer,
retain rejected Desktop guidance as the next explicit turn, warn on abnormal
unapplied guidance, use an explicit fail-closed preview gate, and verify
twelve concurrent Desktop sessions through disk reload.
Verification:
Passed focused race tests, root and Desktop Go suites, Go vet, frontend
typechecks and focused tests, frontend production build and bundle budgets,
and all eight cache-guard scenarios.
Problem:
The copy implementation from merged PR #7007 had only synthetic KaTeX fixtures and did not exercise Reasonix's real Markdown normalization pipeline.
Root cause:
A clean Git merge could still leave the renderer annotation and selection-copy contracts semantically incompatible.
Fix:
Render normalized Markdown through the production remark and rehype plugins, select the resulting DOM, and assert that copy returns the original pipe and percentage formulas.
Verification:
- pnpm exec tsx src/__tests__/message-selection-copy.test.ts (11 passed)
- pnpm exec tsx src/__tests__/math-golden.test.ts (252 passed)
- pnpm typecheck
- pnpm test:typecheck
Problem:
The PR base advanced with session recovery changes and the merged LaTeX selection-copy implementation.
Root cause:
The feature branch still targeted an older main-v2 snapshot and could not validate the final renderer-to-copy contract.
Fix:
Merge the latest origin/main-v2 without rewriting feature history.
Verification:
The incoming paths do not overlap the renderer changes; focused and full validation will run on the merged tree.
Problem:
Currency amounts wrapped in punctuation could be rendered as math, and KaTeX annotations exposed normalized TeX instead of the user's source.
Root cause:
Currency matching read raw wrapper punctuation, while parser-safety and KaTeX normalization rewrites were destructive before selection-copy could observe the original formula.
Fix:
Normalize only currency boundaries, carry parser-safe inline source through the AST, and restore original TeX into KaTeX annotations and data attributes in both live and export renderers.
Verification:
- pnpm exec tsx src/__tests__/math-golden.test.ts (252 passed)
- pnpm test
- pnpm typecheck
- pnpm test:typecheck
- pnpm build
- pnpm check:css
Context:
The official main-v2 branch advanced after the PR branch was published. Merge the live base without rewriting the already-pushed fork history.
Compatibility:
The upstream LaTeX copy changes touch no files modified by the guarded execution fix, so both histories remain intact.
Verification:
- merge completed without conflicts
- full Go, Desktop, frontend, typecheck, and cache guard suites are rerun before PR publication
Problem:
The PR's lint job failed because the StepFun regional endpoint fix left an unused preset-matching helper behind.
Root cause:
The migration was intentionally converted to a no-op so user-selected regional endpoints remain stable, but its former predicate was no longer referenced.
Fix:
Remove the obsolete helper without changing provider loading or persistence behavior.
Verification:
- go test ./internal/config
- git diff --check
Problem:
- Existing CLI and Desktop manifests predate exact release-note URLs, RC notes had no supported catalog path, and unpublished exact routes could 404.
- The Preview SignPath call-graph test also retained the old allowlist.
Root cause:
- New manifest validation and byte comparisons did not distinguish legacy persisted objects from newly generated candidates.
- Standalone RCs were treated as public changelog identities, and version routes were generated only after publication markers.
Fix:
- Add strict and legacy validation plus semantic comparators that normalize only the missing release-notes field and preserve immutable Desktop manifests.
- Reuse Stable notes for standalone RCs, generate safe unpublished redirects, and update the SignPath contract test.
- Add focused workflow, manifest, route, and RC regression coverage.
Verification:
- bash scripts/release-workflows.test.sh
- go test ./... and go vet ./...
- desktop go test ./... and go vet ./...
- site npm test and npm run build
- node --test scripts/release-notes.test.mjs
- actionlint with the repository CI allowlist
- live v1.18.0 CLI and Desktop manifest compatibility checks
Problem:
Permission modes, Auto Guard recovery, late steer handling, and desktop submission paths could disagree about whether work should run, prompt, or stop. Concurrent session display writes and unbounded parallel task batches also exposed reliability gaps.
Root cause:
The affected paths used separate fallback assumptions, lost explicit rule provenance, accepted empty provider input behind visible display text, replayed stale guidance, and lacked serialization or admission bounds at key state boundaries.
Fix:
Unify permission precedence and matched-rule explanations, fail closed for headless Ask, require exact approval for dynamic Bash, preserve legacy PowerShell rules, cancel dismissed Ask turns, reject empty provider input while preserving skill-only turns, classify safe verification commands, recover stale edits automatically, persist late steer as local-only, serialize display sidecar updates, and cap parallel task batches.
Compatibility:
Existing session and wire formats remain readable; legacy Set-Content, Add-Content, and Out-File rules migrate transparently; unrelated Verb-Noun tool rules retain their meaning; provider prompt prefixes and tool schemas are unchanged.
Verification:
- go test -p 1 ./...
- cd desktop && go test ./...
- cd desktop/frontend && pnpm test
- cd desktop/frontend && pnpm typecheck
- git diff --check
Problem:
Reasonix 1.18 regressed session recovery, mid-turn steering, permission matching, provider configuration, capability routing, and several Desktop workflows.
Root cause:
State ownership and replay paths were inconsistent across rebuilds, dynamic Bash safety had no explicit advanced override, trusted instruction imports were over-confined, and multiple UI and routing surfaces lacked deduplication or stable interaction boundaries.
Fix:
Preserve tab-local session state and raw replay content, reconcile steer and approval ownership, bound parallel work, harden permission and provider behavior, deduplicate capability routes, and repair the affected Desktop interactions with focused compatibility coverage.
Verification:
- go test -p 1 ./...
- cd desktop && go test -p 1 ./...
- pnpm build
- focused and remaining frontend suites
- ./scripts/cache-guard.sh
Problem:
Migrated Desktop transcripts without a system message still lost new turns on tab switches. PowerShell compatibility also reinterpreted generic hyphenated tool names, and public docs described the old permissive headless behavior.
Root cause:
The Desktop resume wrapper returned early for prompt-less sessions, while cmdlet inference lived in the global permission parser. Existing coverage exercised direct storage instead of the Desktop resume and switch lifecycle.
Fix:
Restore the deterministic system prompt while preserving the loaded persistence baseline, add a migrate/resume/turn/switch/reload regression, scope legacy cmdlet expansion to additive deny rules, and align CLI and site documentation.
Verification:
- go test ./... -count=1
- cd desktop && go test ./... -count=1
- go vet ./...
- cd desktop && go vet ./...
- focused race tests for migrated sessions, prompt stability, and permission rules
- ./scripts/cache-guard.sh
- pnpm --dir site test
- pnpm --dir site build
Problem:
Explicit paste recovery truncated bodies containing a matching end marker, and a controller that adopted newer history could reuse paste IDs already present in that transcript.
Root cause:
The frame parser accepted the first matching end marker without validating the label line count, while the TUI paste allocator only learned historical IDs during initial construction.
Fix:
Select frame boundaries whose recovered body matches the encoded line count, and merge authoritative controller history into the wrap-safe ID allocator before every allocation.
Verification:
- go test ./internal/cli -count=1
- go test -race ./internal/cli -count=1
- env -u DEEPSEEK_API_KEY go test ./...
- (cd desktop && go test ./...)
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check
Co-authored-by: SivanCola <[email protected]>
Context: Stable and Preview need independent reviewed identities while CLI, Desktop, npm, and changelog publication remain one protected release event.
Changes:
- Add a protected Preview orchestrator with a shared ordinal and SignPath preflight.
- Bind reviewed release notes to immutable all-surface publication markers.
- Route downloads and changelog archives by channel with accessible theme-aware navigation.
- Harden release authorization, manifests, tests, and operator documentation.
Compatibility:
- Legacy catalog entries without managed status remain readable and public.
- Existing Stable pointers remain unchanged, and Preview continues mirroring the legacy canary pointer for older Desktop clients.
- No persisted user state or Wails JSON contract changes.
Verification:
- bash scripts/release-workflows.test.sh
- node --test scripts/release-notes.test.mjs
- cd desktop && go test ./cmd/sign ./internal/update
- cd site && npm test
- cd site && npm run build
- actionlint for all release workflows
Integration: Sync main-v2 at e498c4a7 without rewriting the published PR branch.
Scope: The incoming changes repair pasted-text session history and do not overlap the Markdown math policy.
Verification: desktop/go test ./... passed and the staged merge passes git diff --check.
Problem: The classifier documentation still described the pre-review behavior where paired numbers defaulted to math.
Fix: Document the implemented literal-by-default policy, positive mathematical context requirement, and contextual currency repair.
Verification: The comment-only diff passes git diff --check.
Problem:
Selecting rendered math from a resumed session or replayed branch copied only the Unicode display glyph instead of the original LaTeX source.
Root cause:
The copy transcript path marker-rendered live Markdown blocks but reused the already-rendered text for replay bundles, so historical assistant formulas had no source spans.
Fix:
Share replay traversal between visible and clipboard renderers, inject the marker-aware assistant renderer for copy operations, and assign unique prefixes across replayed assistant messages. Add coverage for ordinary and local recovery history.
Verification:
- go test ./internal/cli -count=1
- go test -race ./internal/cli -run 'TestSelectedText|TestReplaySections' -count=1
- go test -p 2 ./...
- go vet ./...
- cd desktop && go test ./...
- pnpm test:typecheck
- pnpm exec tsx src/__tests__/message-selection-copy.test.ts
- pnpm build
Problem:
Session updates could be lost after migration or concurrent tab writes. Late steer text could execute on a later turn, headless runs could silently approve writes, PowerShell deny rules missed cmdlets, and parallel_tasks accepted unbounded batches.
Root cause:
Append durability and display sidecar updates lacked fsync and serialized read-modify-write. Unconsumed steers were persisted as future user turns. Explicit headless Ask inherited an allow-on-nil bootstrap gate. Bare cmdlets were parsed as tool names, and task batch length was not bounded.
Fix:
Fsync append events and lock display sidecar updates. Persist unapplied steers as provider-excluded local history. Fail closed for explicit headless Ask and add -y/--auto. Normalize PowerShell cmdlet rules, cap parallel_tasks at 64, and add migration, compatibility, and concurrency regressions.
Verification:
- ./scripts/cache-guard.sh
- go test ./...
- cd desktop && go test .
- go vet ./...
- cd desktop && go vet .
- focused go test -race coverage for sessions, steering, permissions, and parallel tasks
Co-authored-by: ipanta <[email protected]>
Integration:
Bring the contributor branch onto the current main-v2 baseline without rewriting its public history.
Resolution:
Accept the latest goal lifecycle, DeepSeek vision, rich-link, session telemetry, and pasted-text fixes. None overlaps the TUI replay-copy implementation being repaired next.
Verification:
The merge completed without conflicts and the integrated tree will be validated together with the focused replay-copy fix.
Integration: Sync main-v2 at 8ccf5bb4 after the review fixes without rewriting the published PR branch.
Scope: The incoming change is limited to the Goal control path and does not overlap the Markdown math policy.
Verification: go test ./internal/control passed and the staged merge passes git diff --check.
Problem: Moving all inline math normalization after Markdown parsing let GFM split pipe-bearing formulas into table cells, while numeric-only spans without recognized local cues could turn ordinary prices into KaTeX.
Root cause: GFM table tokenization runs before remark AST transforms, and the numeric policy defaulted ambiguous pairs to math while reading only direct text siblings.
Fix: Protect pipe-bearing inline math at the syntax boundary, default ambiguous numeric pairs to literal text, require positive range, operator, or scientific-unit context for numeric math, and derive multilingual currency context from source positions across inline formatting.
Verification: pnpm test; pnpm typecheck; pnpm test:typecheck; pnpm build; go test ./... in root and desktop; 240 math golden cases; 28 rich-link cases; bundle budgets; git diff --check.
Problem: A label-like history entry at the platform integer limit made the resumed paste counter overflow to a negative value.
Root cause: History initialization incremented parsed IDs directly, and paste/image allocation used unchecked counter increments.
Fix: Track historical paste IDs and allocate through a wrap-safe counter that skips identifiers already in use.
Verification: go test ./internal/cli -count=1; go test -race ./internal/cli -count=1; env -u DEEPSEEK_API_KEY go test ./...; go vet ./...; desktop go test ./...; ./scripts/cache-guard.sh.
Co-authored-by: SivanCola <[email protected]>
Integration: Sync main-v2 at 0c0422c8 into the PR branch without rewriting published history.
Conflict resolution: Preserve RichMarkdownLink from #6976 while retaining the shared reasonixRemarkPlugins chain used by live rendering and session export.
Verification: Conflict markers are absent and the staged merge passes git diff --check; full branch verification follows with the review fixes.
Problem:
Sessions written before RawContent existed still reopen expanded pasted-text blocks after an upgrade.
Root cause:
History display returned the resolver fallback immediately when RawContent was empty, bypassing legacy pasted-block collapse.
Fix:
Use sanitized Content as the replay source for content-only transcripts, preserve sidecar precedence, and cover the real session save/load path.
Verification:
- focused Desktop history recovery tests
- go test ./...
- cd desktop && go test ./...
- ./scripts/cache-guard.sh
Co-authored-by: SivanCola <[email protected]>
Problem:
An old Goal turn could be admitted correctly, then have its assistant output applied to a replacement Goal when SetGoal ran during the model turn. The stale marker could complete or otherwise advance the new Goal.
Root cause:
The lifecycle epoch guarded continuation input admission, but the subsequent advance() call did not verify that the assistant output still belonged to the Goal that started the turn.
Fix:
Capture the continuation epoch before every Goal-owned real, edited, and subagent turn, carry the admitted epoch across synthetic continuations, and validate it under the goalMachine lock before applying any assistant marker. Add deterministic regressions for both ordinary Goal turns and synthetic continuations replacing the Goal during runner execution.
Verification:
- focused Goal lifecycle regression tests
- ./scripts/cache-guard.sh
- go test -count=1 ./internal/control
- go test -race -count=1 ./internal/control
- go test ./...
- git diff --check
Co-authored-by: SivanCola <[email protected]>
Problem: Inline dollar classification ran before Markdown parsing, conflating paired numeric math with currency and relying on HTML entities to bypass remark-math.
Root cause: A global regex owned both syntax recognition and Reasonix product policy without AST code boundaries or surrounding-node context.
Fix: Add a shared remark AST policy using unist-util-visit, classify parsed inline math as math, currency, or literal, remove the entity escape path, and share the same plugin chain between live rendering and session export. Adapt the paired-number and one-sided-equality cases from #7054.
Verification: pnpm test; pnpm typecheck; pnpm test:typecheck; pnpm build; git diff --check.
Co-authored-by: Xingbo Zhao <[email protected]>
Integration:
Bring the contributor branch up to main-v2 at d541c17e after adding the paste recovery and Qwen preset compatibility fixes. The merge completed without conflicts and preserves the PR-specific behavior.
Verification:
env -u DEEPSEEK_API_KEY go test ./...
(cd desktop && go test ./...)
go vet ./...
go test -race ./internal/agent -run 'Test.*(Snapshot|SaveRewrite|Foreign|Interrupted|Concurrent)' -count=1
./scripts/cache-guard.sh
git diff --check
Co-authored-by: SivanCola <[email protected]>
Problem:
Orphaned paste recovery removed significant whitespace and silently chose the newest conflicting legacy label. Existing Qwen preset installs also kept zero context limits after the curated defaults changed.
Root cause:
The paste parser trimmed framed bodies and stopped at the first history match, while the Qwen preset update had no load-time compatibility migration.
Fix:
Recover only a unique user-authored paste body, preserve its bytes inside the framing newlines, and migrate untouched official Qwen installs while retaining customized provider and model values.
Verification:
go test ./internal/cli ./internal/config -count=1
git diff --check
Co-authored-by: SivanCola <[email protected]>
Problem:
Compact display text replaced complete raw input for pasted and reference turns, degrading capability routing and previous-client replay.
Root cause:
runRefTurnWithResolverSync reused display as the orchestrator raw value even though raw is also the semantic and persistence contract.
Fix:
Restore complete input as raw, keep sidecar and legacy display recovery, and add routing and compatibility assertions.
Verification:
- go test ./...
- cd desktop && go test ./...
- ./scripts/cache-guard.sh
- frontend edit replay tests (17/17)
- frontend pasted block tests (9/9)
Co-authored-by: SivanCola <[email protected]>
Integration: Bring PR #6976 forward to main-v2 at d541c17e without rewriting the contributor branch history.
Compatibility: Preserve the session rebind and overlapping-interface fixes introduced after the prior synchronization; the rich-link implementation remains isolated to Markdown presentation.
Verification: Rerun focused frontend tests, type checking, CSS validation, and production build after this merge.
Integration: Synchronize PR #6976 with main-v2 at 0ac122a3 while preserving the contributor history.
Conflict resolution: Keep main-v2 terminal, goal, bundle-budget, Markdown bare-rendering, and performance changes; add the rich-link rendering test to the current frontend test list.
Verification: rich-link, selection-copy, Mermaid rendering, and render-optimization tests pass; TypeScript, CSS checks, production build, bundle budgets, and narrow-width browser behavior pass.
Integration:
Bring the contributor branch onto the current main-v2 baseline without rewriting its public commit.
Resolution:
Accept the latest Desktop session reattach and status-bar layout fixes from main-v2; they do not overlap the LaTeX clipboard implementation.
Verification:
The merge completed cleanly. Focused clipboard tests and repository checks are rerun on the integrated tree.
Co-authored-by: SivanCola <[email protected]>
Problem:
CI reported SA4006 because the no-marker branch assigned the loop position immediately before breaking.
Fix:
Remove the dead assignment while preserving the branch's existing output and control flow.
Verification:
- go test ./internal/cli -count=1
- go test -race ./internal/cli -run 'TestSelectedText|TestLanguageCommandRefreshesCurrentController|TestCurrencyCommandPersistsAndRefreshesCurrentController' -count=1
- go vet ./internal/cli
- golangci-lint v2.12.2: 0 issues
Co-authored-by: SivanCola <[email protected]>
Problem: Compact GitHub labels replaced selectable URLs, long external links could not wrap, and parser-only tests did not cover rendered accessibility or copy behavior.
Root cause: The renderer swapped the DOM text for a short label and applied white-space: nowrap to every enriched link.
Fix: Keep the original URL as DOM text, expose the compact label through presentation-only CSS, add contextual accessible names, allow long labels to shrink and wrap, and exercise the rendered link contract.
Verification: pnpm exec tsx src/__tests__/github-link-rendering.test.tsx; pnpm typecheck; pnpm check:css; git diff --check.
Integration:
Bring the contributor branch onto the current main-v2 baseline without rewriting its public commit.
Resolution:
Accept the remote workbench runtime fix from main-v2; it does not overlap the LaTeX clipboard implementation.
Verification:
The merge completed cleanly. Focused clipboard tests and repository checks are rerun on the integrated tree.
Co-authored-by: SivanCola <[email protected]>
Problem:
The official-endpoint guard treated every current and future DeepSeek model as permanently text-only, so an explicitly configured future multimodal model could never receive images.
Root cause:
Vision capability provenance was discarded during provider assembly, leaving the provider unable to distinguish stale provider-wide vision=true from a deliberate per-model opt-in.
Fix:
Keep official DeepSeek models text-only by default, carry positive model-scoped vision provenance from vision_models or model_overrides into provider assembly, and permit image parts only for that explicit opt-in. Add config, provider-serialization, and boot-assembly regressions.
Verification:
- focused config/provider/boot tests passed
- cache guard passed
- root Go suite passed with the live DeepSeek key unset
- focused Desktop tests passed
- frontend composer image tests passed (15/15)
Co-authored-by: SivanCola <[email protected]>
Problem:
A synchronous Notice callback could replace the active Goal after the initial continuation check but before the synthetic turn was composed, pairing an old intercept with the replacement Goal.
Root cause:
Continuation validation and the Goal snapshot used by turn composition were separate operations on opposite sides of externally callable event emission.
Fix:
Revalidate and capture the Goal, AutoResearch task, and delivery scope atomically at synthetic-turn admission. Abort stale continuations and keep admitted turns bound to the captured lifecycle snapshot. Update the merged blocked-signal test to the result-owned intercept model and add a deterministic Notice callback regression test.
Verification:
- go test ./internal/control
- go test -race ./internal/control
- go test ./...
- git diff --check
Co-authored-by: SivanCola <[email protected]>
Problem:
Desktop copy could discard prose around a single formula, while the TUI reused display coordinates against a separately wrapped raw transcript. The original commit also dropped the /currency dispatcher and discarded the /language rebuild command.
Root cause:
The desktop path performed global string substitution instead of respecting DOM range boundaries. The TUI assumed rendered Unicode and LaTeX source had identical line and column geometry.
Fix:
Reconstruct desktop selections from ordered KaTeX DOM ranges and MathML annotations. Re-render semantic CLI Markdown only when copy is requested, attach zero-width source markers to the identical visible rendition, and map selected display cells back to formula spans. Restore both runtime-setting dispatchers and add focused regressions for partial, mixed, duplicate, and wrapped selections.
Verification:
- go test ./internal/cli -run 'TestSelectedText|TestLanguageCommandRefreshesCurrentController|TestCurrencyCommandPersistsAndRefreshesCurrentController|TestRenderInlineMath|TestCopyToClipboard' -count=1
- go vet ./internal/cli
- pnpm exec tsx src/__tests__/message-selection-copy.test.ts
- pnpm typecheck
- pnpm test:typecheck
- git diff --check
Co-authored-by: SivanCola <[email protected]>
Integration:
Bring the contributor branch onto the current main-v2 baseline without rewriting its public commit.
Resolution:
Preserve the stable-section Markdown renderer introduced on main-v2. The clipboard implementation reads KaTeX source from the selected DOM instead of adding a render-time annotation pass.
Verification:
The conflict was resolved semantically; the final merged tree is validated after the focused fix is applied.
Co-authored-by: SivanCola <[email protected]>
Problem:
Legacy sessions can still reopen expanded pasted-text payloads, and edit replay can resubmit provider-only capability, goal, hook, and resolved-reference context.
Root cause:
History always preferred non-empty RawContent without distinguishing old expanded values from the new compact display format, while SubmitText copied the full provider-facing Content.
Fix:
Prefer persisted display sidecars, collapse legacy expanded paste blocks when no sidecar exists, and sanitize replay text down to user-authored content. Keep display and pagination decisions on the same recovery path and cover session restart plus ref-turn raw persistence.
Verification:
- go test ./...
- (cd desktop && go test ./...)
- ./scripts/cache-guard.sh
- frontend edit-replay and message-pasted-blocks tests
Co-authored-by: SivanCola <[email protected]>
Problem:
Reattaching a detached target over an idle visible source closed the source controller but left its shared plugin-host reference counted. Repeated session switches could accumulate references and keep MCP host processes alive.
Root cause:
applyRuntimeTab replaced the visible tab SharedHostKey before the idle source teardown captured it, so the caller could no longer release the source-owned reference.
Fix:
Transfer the idle source host key with its controller, sink, and lease, then release it after controller shutdown outside App.mu. Keep active-source ownership unchanged and add a regression that verifies the shared-host refcount drops from two to one.
Verification:
- cd desktop && go test . -run Test\(RebindTabToDetachedSessionReleasesIdleSourceSharedHost\|RebindTabToDetachedSessionPreservesRunningSourceRuntime\|CanReclaimRejectsDetachedRuntimeOwner\|RebindTargetLeaseFailureKeepsSourceRuntimeAtomic\|RebindPostLeaseValidationFailureRollsBackCandidate\|BuildTabControllerReattachesDetachedSessionRuntime\) -count=1
- cd desktop && go test -race . -run Test\(RebindTabToDetachedSessionReleasesIdleSourceSharedHost\|RebindTabToDetachedSessionPreservesRunningSourceRuntime\) -count=1
- cd desktop && go test ./...
Co-authored-by: SivanCola <[email protected]>
Problem:
The docked status bar change left an empty row in IM detail views, placed narrow classic terminals over chat, and retained duplicate workbench padding.
Root cause:
Grid rows referenced the always-nonzero dock token instead of the visibility-aware status bar height, while responsive row overrides conflated chrome-visible and chrome-hidden layouts.
Fix:
Use --statusbar-height for dock rows, collapse it through layout--statusbar-hidden, restore separate responsive terminal rows, remove residual workbench padding, add regression contracts, and normalize the CSS formatting introduced by the original edit.
Verification:
- pnpm test
- pnpm check:css
- pnpm typecheck
- Browser computed-style and geometry checks at 800px and 1200px
- git diff --check
Co-authored-by: SivanCola <[email protected]>
Problem: Session hydration could issue duplicate context requests, while persisted last-usage fields could be overwritten by planner or subagent telemetry.
Root cause: The fallback effect raced the authoritative hydration request, and recordUsage updated executor fallback fields for every usage source.
Fix: Keep persisted Last* context scoped to executor usage, remove the competing active-tab fetch, and add compatibility, rebind fallback, and hydration coalescing regression coverage.
Verification: go test ./...; pnpm test:typecheck; tsc --noEmit; focused frontend hydration and context tests.
Co-authored-by: SivanCola <[email protected]>
Problem:
- PR 6971 had fallen behind main-v2 and conflicted with the new Kimi K3 preset helper.
Root cause:
- Both branches added provider override helpers at the same insertion point.
Fix:
- Merge current main-v2 and retain both the Kimi K3 and Qwen per-model overrides without rewriting contributor history.
Verification:
- go test ./internal/agent ./internal/cli ./internal/config
- ./scripts/cache-guard.sh
Integration:
- Update the contributor branch to the latest main-v2 without changing the catalog fix.
- Preserve the empty effort-level normalization and its end-to-end regression coverage.
Verification:
- go test ./internal/remote/workbench/runtime -run ^TestWorkspaceCatalogEmptyEffortLevelsEncodeAsArray$ -count=1
- go test -race ./internal/remote/workbench/runtime -run ^TestWorkspaceCatalogEmptyEffortLevelsEncodeAsArray$ -count=1
- go test ./internal/remote/protocol -count=1
- REASONIX_RELEASE_CACHE_GUARD=1 go test ./...
- cd desktop && go test ./...
- go vet ./...
- go build ./...
- git diff --check
Problem:
- Same-revision snapshot recovery could overwrite an interrupted foreign write.
- Paste recovery mutated loaded history, reused labels after resume, and trusted assistant text.
- Mixed Qwen catalogs inherited a provider-wide 1M context budget.
Root cause:
- Revision equality was treated as ownership without checking the persisted digest.
- Recovery lived in the persistence layer while paste IDs were process-local.
- Qwen context metadata was configured only at provider scope.
Fix:
- Require persisted-state ownership for non-prefix snapshots and preserve foreign bytes.
- Keep loaded history byte-stable, recover labels only on explicit resubmission, and continue IDs from history.
- Add per-model context overrides for smaller Qwen catalog models.
Verification:
- go test ./internal/agent ./internal/cli ./internal/config
- go test -race ./internal/agent -run snapshot recovery cases
- ./scripts/cache-guard.sh
- full repository suite passed except an opt-in live probe using an expired DEEPSEEK_API_KEY; the package passed with that variable unset
Co-authored-by: SivanCola <[email protected]>
Contract:
Fresh desktop sessions must persist their scope, workspace root, and topic before controller reconciliation, including when the session directory is not yet discoverable.
Coverage:
- Verify project and global binding metadata at creation.
- Verify fallback reconciliation preserves project scope outside known directories.
- Verify corrupt sidecars fail visibly and rejected bindings leave no session artifacts.
- Verify read-modify-write preserves existing BranchMeta persistence and listing fields.
Verification:
- go test -count=1 . -run 'TestPinNewEmptySessionBranchMeta|TestPinnedSessionMetaKeepsProjectBindingOutsideKnownDirectories|TestPinSessionBranchMeta'
- git diff --check
Co-authored-by: SivanCola <[email protected]>
Problem:
New desktop sessions could continue into controller startup after their workspace binding metadata failed to persist, leaving the original project-to-global reclassification window open.
Root cause:
The pinning helper ignored BranchMeta read and write errors and performed an unlocked read-modify-write at each call site.
Fix:
Serialize the BranchMeta update with the session metadata lock, normalize scope/root values, propagate failures from every new-session call site, and remove any newly created unbound session artifacts before returning.
Verification:
- go test -count=1 . -run 'TestEnsureBlankTab|TestTrashTopicFallbackCreatesUnindexedBlank|TestTransientFallbackIndexesOnFirstUserTurn|TestOpenProjectTab'
- git diff --check
Co-authored-by: SivanCola <[email protected]>
Problem: PR #6961 returned intercepts from advance(), then wrote them back through a second lock acquisition. A concurrent goal replacement, stop/resume, or newer FSM step could therefore consume stale output from an older lifecycle.
Root cause: goalAdvanceResult carried no identity for the machine state that produced it, and the orchestrator round-tripped the output through interceptMsg.
Fix: remove the pending intercept field, consume the returned intercept directly, and validate a monotonic continuation epoch before starting the synthetic turn. Lifecycle transitions and newer FSM steps invalidate older results.
Verification: go test ./internal/control; go test -race ./internal/control; go test ./...; git diff --check.
Co-authored-by: SivanCola <[email protected]>
Context: Bring PR #6961 onto the current review baseline before changing the goal FSM output contract.
Integration: Preserve current goal snapshot, transactional persistence, raw-input propagation, and large-session fixes while keeping the contributor refactor isolated for follow-up changes.
Verification: Resolve the merge without conflicts and validate the resulting focused PR diff before push.
Problem:
Reattaching a detached target session unconditionally closed the visible source controller, cancelling its active turn and background jobs.
Root cause:
The fast reattach path bypassed the normal rebind active-runtime detachment semantics and retired the source after attaching the target.
Fix:
Atomically move an active source runtime into detachedSessions while attaching the detached target. Only idle source runtimes are closed and released. Add regression coverage for dual-running session switches and detached-owner lease reclaim.
Verification:
- cd desktop && go test . -run Test\(RebindTabToDetachedSessionPreservesRunningSourceRuntime\|CanReclaimRejectsDetachedRuntimeOwner\|RebindTargetLeaseFailureKeepsSourceRuntimeAtomic\|RebindPostLeaseValidationFailureRollsBackCandidate\|BuildTabControllerReattachesDetachedSessionRuntime\) -count=1
- cd desktop && go test -race . -run Test\(RebindTabToDetachedSessionPreservesRunningSourceRuntime\|CanReclaimRejectsDetachedRuntimeOwner\) -count=1
- cd desktop && go test ./...
Co-authored-by: SivanCola <[email protected]>
Problem:
The blocked lifecycle coverage only asserted the final status, so it could not detect an idle reminder replacing the normal continuation prompt.
Root cause:
No test exercised a blocked marker while the idle counter was one turn below its threshold.
Fix:
Add a focused goal-machine regression that seeds the near-threshold state and verifies an explicit blocked signal produces no idle intercept.
Verification:
- go test ./internal/control -run '^TestGoalBlockedSignalDoesNotTriggerIdleIntercept$' -count=1
- go test ./internal/control -run 'Goal' -count=1
- go test -race ./internal/control -run '^TestGoalBlockedSignalDoesNotTriggerIdleIntercept$' -count=1
- go test ./...
Problem:
DeepSeek's official chat API rejects image_url content parts, while the original fallback treated image data URLs as file paths and could copy large base64 payloads into message text.
Root cause:
Endpoint image capability was conflated with the DeepSeek reasoning protocol, and the text fallback assumed image values were filesystem paths instead of resolved data URLs.
Fix:
Disable direct image input for official DeepSeek endpoints at both capability resolution and provider assembly, keep explicit vision support for custom DeepSeek-compatible gateways, remove the text fallback, and add request-shape regressions for user and tool images.
Verification:
- focused config, provider, and controller tests passed
- cache guard passed
- root Go suite passed with the unavailable live credential unset
- desktop image capability and frontend composer image tests passed
Co-authored-by: SivanCola <[email protected]>
Problem:
Compatible gateways that return Anthropic-style usage counters were parsed, but input_tokens was treated as the complete prompt and total_tokens stayed zero. Cache writes also omitted ordinary uncached input from miss accounting.
Root cause:
Anthropic-style usage splits the prompt across input_tokens, cache_creation_input_tokens, and cache_read_input_tokens, while Reasonix expects complete prompt, hit, miss, and total counters.
Fix:
Sum all three input components, derive missing totals, preserve DeepSeek and OpenAI field precedence, replace struct-only assertions with decoded JSON fixtures, and remove the unrelated desktop version change from the PR diff.
Verification:
- go test ./internal/provider/openai -run 'TestNormaliseUsage(AnthropicStyleFallback|DeepSeekShape|MiMoShape)$' -count=1
- go test ./internal/provider/openai ./internal/provider
- go test -p 4 ./...
- git diff --check
Co-authored-by: SivanCola <[email protected]>
Update the contributor branch to the latest review baseline so already-merged Windows filename work and the build-version placeholder no longer appear in the PR diff.
Move the StatusBar out of the footer and into its own dedicated CSS
Grid row so the sidebar and workspace panel no longer extend behind
the semi-transparent bottom status bar.
- Extract <StatusBar> from the footer and place it as a direct child
of the .layout grid, wrapped in the existing sidebarImDetailConnection guard.
- Change the layout grid from 2 rows (chrome + content) to 3 rows
(chrome + content + statusbar): the statusbar row uses
var(--statusbar-dock-height) so it scales with the interface text size.
- Remove position:fixed from :root[data-theme-style] .statusbar — the
grid row now keeps it anchored to the viewport bottom.
- Properly place the statusbar in every grid variant:
normal (row 3), workbench-chrome-hidden (row 2), terminal-open (row 3
or 4), and in the narrow-viewport media query.
- Hide the statusbar row in creation mode (.app--creation .statusbar)
since that layout has no bottom bar.
- Drop the --statusbar-height compensation from the sidebar and
workbench-dock bottom padding — the grid row boundary handles the
separation, so the extra padding only created dead space.
- Fix :root[data-theme-style] .footer bottom padding (38px → 10px):
the 38px was reserved for the old fixed statusbar and caused a
visible gap below the composer.
- Let the workspace-panel-resizer fill the full grid-cell height
(100% instead of calc(100% - var(--statusbar-height))) since the
resizer no longer needs to avoid a fixed overlay.
- Update the app-chrome-tabs and workspace-layout tests to match the
new grid-template-rows declarations.
Fixes#7030
The previous fix made both branches of the RawContent check set
hm.SubmitText to m.Content, making the else-if redundant.
Collapse them into a single else branch for clarity.
#1 Root cause: runRefTurnWithResolverSync passed submitText as raw, causing RawContent to store the expanded text instead of the display text. This made history loading leak BEGIN/END blocks into the bubble. #2 Fix SubmitText assignment: set hm.SubmitText to m.Content so the frontend can parse paste blocks. #3 Fix fallback: wrap StripComposePrefixes with StripReferencedContextPrefix to strip file XML blocks when .display.json is missing.
Validating the sweep against live data before enabling it turned up two ways it
would have aged current reports.
#6563 filled the form's version field with 1.0.0 and named the real build,
v1.17.13, further down in the reproduction steps. 1.0.0 shipped, so the
published-tag check could not catch it, and the report would have been swept as
seventeen minors old. Age now comes from the newest released version mentioned
anywhere in the body: a report cannot be older than the newest build it names.
Filtering to published versions keeps Node versions and window sizes out.
Reading the whole body also let feature requests in, and "does it still
reproduce" is a question only a defect report can answer, so the sweep now
requires the bug label.
The target count is unchanged at 160, but the membership is not: six typo'd
reports and seven feature requests left, and old reports whose only version
evidence was in prose came in.
65% of the open tracker has never had a reply, and 160 open bug reports name a
release nine or more minors back — old enough that nobody can say whether the
symptom survived. Leaving them open costs the reports that can be acted on;
closing them on age alone would be a guess dressed up as triage.
So: ask, wait 21 days, close only what nobody answered. A close is "not planned"
with reopening invited, and any comment after the ask — from anyone — takes the
issue back out of the sweep. Severity labels (data-loss, security, crash) and
anything a maintainer already replied to never enter it at all, because a bot
must not time out a judgement a human already made.
The version check is against the published tag list, not the version format. A
dry run first targeted six issues reporting "1.7.18", "1.7.13" and "1.7.10":
1.7.x only ever reached 1.7.0, so those are dropped digits from 1.17.x — current
releases that parse cleanly as eleven-minor-old ones. Six current reports would
have been swept as ancient.
Posting stays behind the STALE_SWEEP_ENABLED repository variable, so merging
this cannot start commenting on its own.
A PR writing "Fixes #N" auto-closes its issue on merge. One writing "Refs",
"Related" or "Addresses" does not, so the report stays open waiting for a
follow-up after the release that nobody sends. Auditing the last 400 merged
PRs found 33 open issues referenced by shipped work; only three could be
closed on the evidence, and most of the rest were blocked on nothing but an
unanswered "please re-test".
On every published release this now diffs against the previous tag in the
same series, collects the issues those PRs referenced, and asks each still-open
reporter to verify. It never closes anything — deciding a report is resolved
stays human.
Two traps the audit hit are pinned by tests rather than left to be rediscovered:
- "owner/repo#123" is an upstream reference. Matching it once pulled a
wailsapp/wails issue number in as if it were ours.
- GitHub's issues API answers for pull requests too, so a consolidation PR
listing "PR #5576 by @x" reads as a perfectly good open issue. A dry run
against desktop-v1.18.0 showed 26 of 40 references were pull requests; without
the check the bot would have asked their authors to re-test.
The PR template now says where to put the keyword, since a "- Fixes#123" list
item is exactly the silent no-op that produced this backlog.
A /theme switch repainted every colour in one frame, which reads as a
glitch rather than a deliberate change. The new palette now travels
across the frame from left to right over roughly 290ms.
Both frames are rendered once when the sweep starts, so the cost is two
renders rather than two per animation step; the transcript is frozen for
the duration, and a switch during a running turn skips the animation
rather than freeze streaming output. No-colour terminals, terminals
under 24 columns, and switches that resolve to the current palette also
fall through to the instant repaint.
Rows are composed to an exact cell count. Slicing on an odd column
cannot split a double-width rune, so a CJK transcript otherwise runs a
row one cell long and the boundary zigzags down the screen.
Windows Terminal advertises 24-bit colour through WT_SESSION and sets
neither COLORTERM nor TERM_PROGRAM, so the hand-rolled check never
matched it and every theme colour rendered as its 256-colour fallback.
The same gap hid kitty, alacritty, ghostty, foot, rio, st and contour,
and never downgraded tmux or screen.
Colour capability was also split across two globals that could disagree:
colorEnabled decided on/off from its own NO_COLOR/isatty rules while
supportsTrueColor separately decided fidelity. Both collapse into one
detected profile, and colorOn/trueColorTerminal derive from it, so
CLICOLOR_FORCE and the cmd.exe Windows API path come along for free.
The mode pill bypassed the theme entirely with raw lipgloss colours,
which meant NO_COLOR never suppressed it; it now goes through the same
accessor as everything else.
Terminal background probing was opt-out: thirteen call sites had to
remember a NoProbe wrapper or they would read stdin while the TUI owned
it. Inverted to opt-in, which removes the wrapper and the footgun.
The hand-chosen 256-colour column stays. Delegating it to the library
was tried and reverted: the quantiser maps #222631 to navy 17 at 34x the
distance of the correct grey, and even a proper Lab nearest-neighbour
flattens the dark diff backgrounds to grey, losing the red/green tint
that carries their meaning.
Problem: the full Windows release sweep timed out two ACP end-to-end prompt scenarios after two seconds, leaving their session lease files locked during cleanup.
Root cause: drainPrompt used a two-second idle limit while exercising the complete ACP server, controller, agent, tool-loop, and transcript-persistence pipeline. Loaded Windows runners can exceed that scheduling window without violating the protocol contract.
Fix: keep the end-to-end helper bounded while extending its idle responsiveness window to five seconds and documenting the covered pipeline.
Verification: go test ./internal/acp -run '^TestE2E(ToolTurnAndPersistence|SessionLoad)$' -count=20; go test -race ./internal/acp -run '^TestE2E(ToolTurnAndPersistence|SessionLoad)$' -count=5; go test ./internal/acp
Problem: Windows release runners can take more than one second to schedule asynchronous controller completion during the full desktop suite, causing Fork or Rewind tests to fail before their semantic assertions and leave lease files locked during cleanup.
Root cause: the shared waitNotRunning test helper used a one-second outer bound that was tighter than loaded Windows runner scheduling latency.
Fix: retain the polling invariant while extending the bounded responsiveness window to five seconds and document why the platform-neutral test needs that allowance.
Verification: go test . -run '^Test(Fork|Rewind)KeepsProjectWorkspacePrompt$' -count=20; go test -race . -run '^Test(Fork|Rewind)KeepsProjectWorkspacePrompt$' -count=5; go test ./...
Problem: The existing v1.18.0 catalog covered only the earliest merged work and the automated bilingual generator returned truncated JSON twice.
Fix: Replace the draft with a source-grounded bilingual summary of the full v1.17.21..main-v2 release range, including the Guard automation upgrade note and all major product, reliability, security, and release-channel changes.
Verification: release-notes validate, unit tests, zh/en render, grounded PR reference audit, privacy scan, and git diff --check.
Problem: Windows CI failed because the config-lock registry test required Unix mode 0700 from FileMode.Perm().
Root cause: Windows protects the per-user registry through inherited ACLs and does not represent that ACL through Unix permission bits.
Fix: Skip the Unix-only permission-bit assertion on Windows while retaining the cross-process lock and user-profile registry coverage.
Verification: go test ./internal/config -run 'TestAcquireConfigEditLock|TestLockUserConfigEdits' -count=1; go test ./internal/config; go vet ./internal/config; GOOS=windows GOARCH=amd64 go test -c ./internal/config
* fix(desktop): prevent stale transcript pixels on Windows / 修复 Windows 会话残影
* Harden Windows transcript compositor backing
Problem:
- Windows WebView2 can retain stale TurnActions pixels after React removes old chat nodes in Classic and Workbench layouts.
- A broad transcript rule would also alter History and Creation surfaces, while background packs must keep their existing viewport geometry.
Root cause:
- Theme-pack CSS leaves the main scrolling transcript transparent, so WebView2 has no opaque repaint surface when child layers disappear.
- The earlier #6365 repair covered Creation only.
Fix:
- Scope an opaque Canvas-backed compositor surface to the primary Classic and Workbench transcript.
- Recreate background-pack image, pane opacity, task opacity, and safe-area washes on clipped viewport-fixed pseudo-layers without touching History or Creation.
- Expand the source contract to cover selector scope, stacking, viewport geometry, and all theme scene variants.
Verification:
- pnpm exec tsx src/__tests__/turn-actions-rendering.test.ts (9 passed)
- pnpm check:css
- pnpm typecheck
- git diff --check
- Chromium computed-style, scroll-stability, and Windows/non-Windows visual parity checks
* fix(desktop): reveal Windows theme backgrounds
Problem:
Windows theme packs loaded correctly, but their scene images stayed hidden after making Classic and Workbench transcripts opaque for WebView2 repaint correctness.
Root cause:
The image and overlay pseudo-elements retained negative z-index values, so they painted behind the transcript opaque compositor backing.
Fix:
Use nonnegative image and overlay layers, raise only direct transcript children above both layers, and enforce the ordering in the source-contract regression.
Verification:
Focused rendering regression 10/10; CSS syntax and z-index checks; TypeScript; full frontend test and production build; native Win11 Wails build and WebView2 task scroll plus home new-session validation.
All Qwen provider presets (qwen-cn, qwen-global, qwen-coding-plan-*)
were missing the ContextWindow field, causing the resolver to fall
back to 128k. Qwen 3.x models (qwen3.5-plus, qwen3.6-plus,
qwen3.7-plus, qwen3.7-max, qwen3-coder-plus) all have 1M context
windows per Alibaba's API docs and qwen-code's tokenLimits.ts.
Ref: qwen-code packages/core/src/core/tokenLimits.ts
* fix(guard): bind repair apply to confirmed preview
* Harden Guard preview confirmation binding
Problem: non-interactive apply-plan could accept a bare --yes, while preview IDs omitted derived-state contents and full pending-update identity.
Root cause: confirmation hashes covered only display previews and were checked once before the plan loop, so state-dependent actions could drift after confirmation.
Fix: require a prior preview ID for non-interactive apply-plan, bind opaque file and transaction state, re-check each action immediately before execution, and preserve unrelated multi-action behavior. Update English and Chinese recovery docs.
Verification: go test ./internal/repair ./cmd/reasonix-guard; go test -race ./internal/repair; go vet ./...; go test ./...; git diff --check.
Co-authored-by: SivanCola <[email protected]>
* Serialize confirmed Guard repair mutations
Problem: preview IDs were rechecked before mutation, but another process could still replace a target during the final rename window or race on last-repair.json, losing or mixing undo records.
Root cause: repair actions lacked a shared transaction lock and per-target cross-process locks, while confirmed snapshot/config bytes were read again after approval and replacement writes could overwrite a concurrent recreation.
Fix: serialize repair bookkeeping before canonical target locks, revalidate confirmed state inside the lock and around renames, publish restored content with create-only atomic writes, bind complete pending-update identity, preserve concurrent writers, and keep undo under the same lock order. Add focused coverage for wait/recheck, rename windows, snapshot bytes, unreadable LKG nodes, transaction serialization, and rollback identity.
Verification: go test ./internal/fileutil ./internal/config ./internal/repair ./cmd/reasonix-guard; go test -race ./internal/fileutil ./internal/repair; go vet ./...; env -u DEEPSEEK_API_KEY go test ./...; Windows test-binary cross-compilation for internal/repair and cmd/reasonix-guard; git diff --check.
Co-authored-by: SivanCola <[email protected]>
* Normalize Windows repair lock test paths
Problem: the Windows CI test waited for a raw backslash path while the lock hook receives the canonical lower-case slash form, so the test timed out before exercising the recheck.
Root cause: the regression test compared platform-specific input text instead of the production lock key representation.
Fix: normalize test hook paths with the same Windows case and separator rules used by repair mutation locks.
Verification: focused repair lock tests pass on macOS; Windows amd64 test-binary compilation passes.
Co-authored-by: SivanCola <[email protected]>
* test(jobs): remove StartForSession race
Keep the worker blocked while assertions inspect its state and protect job fields with the existing mutex. This prevents the race job from observing concurrent completion writes when validating accepted input.
Co-authored-by: PaulDing98 <[email protected]>
* fix(repair): bind target identity into Guard preview confirmation
Problem:
- previewId hashed only exported preview fields and content-based state IDs,
so identical reasonix.toml content in two project roots produced the same
confirmation and apply-plan --yes could mutate the unconfirmed project.
- Mutation locks used Abs/Clean only, so symlink path aliases (and macOS
case variants) took different locks for the same real node.
- rollback_update bound only the pending transaction JSON and skipped target
locks, so a stable pending update could still roll back an unconfirmed
live release unit.
- When a target was recreated after quarantine rename, the move was not
recorded in last-repair, so UndoLastRepair could restore an unrelated prior
repair.
Root cause:
- Confirmation identity omitted canonical target identity; lock keys and
rollback binding did not converge on real paths or live release-unit state;
post-rename failure paths returned before persisting the already-moved node.
Fix:
- Embed an irreversible canonical target identity in per-file state IDs used
by previewId, while verifying quarantined nodes against the original path
identity.
- Resolve existing symlink parents and case-fold on Windows/macOS when
building mutation-lock keys and target identities.
- Bind current release-unit file states into rollback preview state, take
shared target locks during prepare/rollback, and re-check under those locks.
- Persist the quarantine/move transaction when a concurrent recreate is
detected so undo restores the confirmed node and retains the rewrite as redo.
Verification:
- go test ./internal/repair/ -count=1
- go test -race ./internal/repair/ ./internal/fileutil/ -count=1
- go test ./cmd/reasonix-guard/ -count=1
- go vet ./internal/repair/
* fix(repair): close symlink, app-bundle, and macOS handoff confirmation gaps
Problem:
- canonicalRepairPath EvalSymlinks'd the full path, so two leaf symlinks
sharing a referent produced the same previewId, and renaming a confirmed
symlink leaf changed its identity and made apply fail.
- App-bundle rollback bound only directory kind/mode for the live .app and
omitted the backup tree, so interior executable drift still accepted an
old previewId and rolled back.
- macOS self-update held mutation locks only while writing the pending
transaction; the actual mv/ditto ran in a shell helper that ignored the
shared flock, racing Guard rollback into mixed bundles.
Root cause:
- Leaf nodes are what Lstat/Rename mutate, but identity resolved through them;
directory release units need tree digests; handoff and repair used different
processes without a shared lock holder over the swap window.
Fix:
- Resolve only parent-directory symlinks and reattach the original leaf name
for mutation locks and preview target identity.
- Bind full tree digests for directory release-unit nodes and include backup
paths in pending-update preview state.
- Replace the macOS shell handoff with a detached self-subprocess that waits
for the old PID, then holds LockRepairMutations around the directory swap.
- Export LockRepairMutations/Timeout for desktop handoff and add regressions.
Verification:
- go test ./internal/repair/ -count=1
- go test -race ./internal/repair/ -count=1
- go test ./cmd/reasonix-guard/ -count=1
- cd desktop && go test -count=1 -run TestMacUpdate .
- go vet ./internal/repair/ and desktop
* fix(repair): secure updater claims and path case locks
Problem:
The detached macOS updater trusted filesystem paths from command-line arguments, and repair identities folded path case by operating system rather than by the target filesystem.
Root cause:
The handoff only checked that a pending-update file existed before swapping bundles. Path canonicalization assumed every macOS and Windows filesystem was case-insensitive.
Fix:
Persist optional handoff paths and owner identity in the pending transaction, claim them under strict pending and target locks, revalidate the staged bundle before swapping, and remove filesystem paths from child arguments. Detect case sensitivity from the actual macOS directory or Windows per-directory flag before folding lock and preview identities.
Verification:
- go test ./internal/repair/ -count=1
- go test -race ./internal/repair/ ./internal/fileutil/ -count=1
- go test ./cmd/reasonix-guard/ -count=1
- cd desktop && go test -count=1 -run TestMacUpdate .
- cd desktop && go test -race -count=1 -run TestMacUpdate .
- go vet ./internal/repair/
- cd desktop && go vet .
- GOOS=windows GOARCH=amd64 go test -c ./internal/repair
* fix(repair): scope case probe helper by platform
Problem:
The Linux golangci-lint job reported existingRepairPathParent as unused.
Root cause:
The helper lived in a common file while its only callers were excluded from Linux builds by Darwin and Windows build tags.
Fix:
Move the helper into a Darwin-or-Windows platform file so it is compiled exactly where the filesystem case probes use it.
Verification:
- golangci-lint run --timeout=5m
- go test ./internal/repair/ -count=1
- cd desktop && go test -count=1 -run TestMacUpdate .
- GOOS=windows GOARCH=amd64 go test -c ./internal/repair
* fix(repair): preserve verified rollback recovery
Problem:
- File rollback could install backup bytes that changed after preflight validation.
- A failed macOS backup restore still cleared pending recovery state and cleanup material.
- NFC and NFD aliases on APFS produced different repair lock and preview identities.
Root cause:
The staged-copy hash was ignored, macOS rollback cleanup was unconditional, and canonical repair paths handled case aliases without filesystem-aware Unicode normalization.
Fix:
Verify the staged rollback bytes against the transaction hash before mutating live targets. Make macOS rollback compensating and retain pending, backup, and staging state when restoration fails. Normalize repair identities to NFD only on APFS and HFS while preserving distinct names on other filesystems.
Verification:
- go test ./internal/repair/ -count=1
- go test -race ./internal/repair/ ./internal/fileutil/ -count=1
- go test ./cmd/reasonix-guard/ -count=1
- cd desktop && go test ./... -count=1
- cd desktop && go test -race -count=1 -run TestMacUpdate .
- go vet ./internal/repair/
- cd desktop && go vet .
- golangci-lint run --timeout=5m
- GOOS=windows GOARCH=amd64 go test -c ./internal/repair
* Harden transactional updater handoffs
Problem:
Repair and self-update paths could continue without a pending-state lock, apply a prepared update after its transaction or release unit changed, or restore an unverified macOS bundle tree. Linux also persisted a CLI sibling that transaction validation rejected.
Root cause:
Pending transitions used a best-effort unbounded lock, file updaters locked paths without claiming the exact prepared transaction, and app-bundle handoff/rollback validation did not bind both the staged tree and original backup tree across the rename window.
Fix:
Use bounded fail-closed pending locks and a consistent pending-to-target lock order. Claim Linux and Windows replacements by version, creation identity, platform, and canonical release-unit paths. Bind macOS staging, original, installed, and backup trees; recover rejected handoffs only when the original bundle is still verified; protect backup paths and reject symlink escapes. Add focused concurrency, drift, rollback, compatibility, and cross-platform tests.
Verification:
- go test ./internal/repair/ ./internal/fileutil/ ./cmd/reasonix-guard/ -count=1
- go test -race ./internal/repair/ ./internal/fileutil/ ./cmd/reasonix-guard/ -count=1
- cd desktop && go test ./... -count=1
- cd desktop && go test -race ./... -count=1
- go vet ./... and cd desktop && go vet ./...
- golangci-lint run --timeout=5m ./...
- Windows amd64/arm64 test-binary compilation for repair, desktop, and update-helper
Co-authored-by: SivanCola <[email protected]>
* Complete fail-closed repair and updater transactions
Problem:
Repair, undo, and self-update entry points could act on state that changed while waiting for locks. File-update completion also needed a crash-safe installed-unit binding, and platform handoffs could execute mutable or incompletely claimed payloads.
Root cause:
Invocation identity was sampled too late or incompletely, mutable pending state doubled as installed state, cleanup paths did not consistently compare ownership, and Windows/macOS handoffs did not bind every source, backup, staging, and release-unit transition.
Fix:
Bind every mutating invocation before lock waits and recheck it under a pending-to-target lock order. Persist create-only installed-state sidecars, preserve recovery material on ambiguity, use atomic no-replace publication, validate complete app and file trees, isolate and hold the verified Windows installer through execution, and make rollback/undo crash-retryable.
Verification:
go test ./internal/repair ./internal/fileutil ./cmd/reasonix-guard -count=1
cd desktop && go test ./... -count=1
Windows amd64 and arm64 update-helper test-binary compilation
gofmt -l internal/repair desktop
git diff --check
Co-authored-by: SivanCola <[email protected]>
* Document fail-closed legacy update recovery
Problem:
Recovery documentation described additive update state as generally backward compatible and a Linux comment still described the obsolete mutable-pending design.
Root cause:
The text was not updated when installed release-unit state moved to a transaction-unique sidecar and legacy destructive actions began requiring explicit ownership proof.
Fix:
Document immutable pending state, crash-safe sidecar recovery, and the distinction between readable legacy transactions and authorization for handoff, healthy commit, or app-bundle rollback.
Verification:
git diff --cached --check
Focused repair and updater tests, full race suites, vet, lint, and cross-platform compilation passed against the merged main-v2 baseline.
Co-authored-by: SivanCola <[email protected]>
* Close updater execution and compensation races
Problem:
- Generic failure-marker writes could let a stale invocation overwrite a newer transaction marker.
- Legacy app rollback could move a symlink backup before rejecting it, and file publish could leave a tampered staged node live.
- Windows self-update could execute a helper that changed after transaction prepare, overwrite a concurrently allocated cache path, or add a portable alias that was absent before.
- macOS updater commands were resolved through PATH.
Root cause:
Mutation ownership was not rebound under the pending lock at every public entry point. Helper validation proved only PE architecture, staged-file verification followed symlinks, and helper cache publication used replace semantics.
Fix:
Rebind generic failure markers after lock acquisition, reject non-directory app backups before mutation, require regular-file staged payloads, compensate failed publication by retaining the rejected node and restoring the verified prior file, and preserve pending recovery state. Bind the Windows helper to the exact prepared release-unit SHA-256, create a unique cache node without replacement, freeze it through process creation, preserve the original portable-alias shape, and use absolute macOS system command paths.
Verification:
- go test ./... -count=1
- go test -race ./internal/repair ./internal/fileutil ./cmd/reasonix-guard -count=1
- cd desktop && go test ./... -count=1
- cd desktop && go test -race ./... -count=1
- go vet ./... and cd desktop && go vet ./...
- golangci-lint 2.12.2 for root and desktop
- cache guard: 8/8 pass
- Windows amd64/arm64 repair, desktop, and update-helper test-binary compilation
- Linux amd64 repair test-binary compilation
Co-authored-by: SivanCola <[email protected]>
* Tidy desktop updater dependencies
Problem:
The desktop CI go-mod-tidy gate failed because the updater no longer imports github.com/minio/selfupdate, but the module files still retained that dependency and its stale transitive checksums.
Root cause:
The transactional updater implementation replaced the selfupdate package without running the desktop module tidy gate afterward.
Fix:
Remove the unused selfupdate requirement and the transitive checksum entries that are no longer reachable from the desktop module graph.
Verification:
- cd desktop && go mod tidy (idempotent)
- cd desktop && go vet ./...
- focused workspace-rebuild timing tests: 10/10 pass
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* Harden prepared repair journals and Windows release-unit signing
Problem:
- Repair mutations could leave ambiguous undo state after a crash between
rename and last-repair.json publication.
- Concurrent writers could replace a pending journal or cleanup node after
ownership checks, turning cleanup into deletion of unowned material.
- Windows portable/installer updates did not bind signed payload identity
through packaging, and Linux release-unit publication did not return
exact installed-state receipts.
Root cause:
Undo bookkeeping treated last-repair.json as the only durable intent, cleanup
used path-based remove after verification, and Windows packaging trusted
unsigned payload layout without a create-only signed manifest contract.
Fix:
- Introduce create-only pending-repair.json write-ahead journals, promote or
discard them under the repair lock, and fail closed on ambiguous recovery.
- Displace cleanup targets to unique siblings, re-verify identity, and never
unlink a pending journal that changed before cleanup.
- Bind Windows signed payload manifests through packaging/signing and require
exact installed-state receipts for Linux release-unit publication.
Verification:
- go test ./internal/repair -count=1 (pass)
- focused security coverage for prepared intent, pending cleanup ownership,
legacy healthy/rollback fail-closed, and macOS staging cleanup
Co-authored-by: SivanCola <[email protected]>
* fix(ci): use resolved version for payload manifest
Problem: The Windows payload manifest step referenced a nonexistent ver step output, so actionlint rejected the release workflow.
Root cause: The build job receives the release version from needs.resolve.outputs.version, but the step used steps.ver.outputs.version.
Fix: Bind the signed payload manifest to the resolved release version output.
Verification: actionlint v1.7.7 and scripts/release-workflows.test.sh pass.
* fix(repair): bind prepared create ownership to published Windows modes
Problem:
- Windows CI rejected undo after a crash following snapshot create: the
restored config file remained because CreatedStateID ownership failed.
- Desktop packaging contract tests still expected steps.ver.outputs.version
after the release workflow switched to needs.resolve.outputs.version.
Root cause:
- Prepared create intents predicted FileMode 0o600, but Windows Lstat reports
regular files as 0444/0666. Undo then treated the published file as an
unowned concurrent write and left it in place.
- The packaging test was not updated with the resolved-version workflow fix.
Fix:
- Predict create-state IDs with platform-published file modes and rebind the
pending journal to the exact on-disk node after AtomicCreateFile.
- Align the Windows signing packaging contract test with the resolve job
version output.
Verification:
- go test ./internal/repair -count=1
- go test ./internal/repair -run TestPreparedCreateStateIDMatchesAtomicCreate,TestApplyRepairPlanPersistsMissingSnapshotTargetBeforeCreate
- cd desktop && go test -run TestWindowsReleaseSignsPayloadBeforeRepackaging .
Co-authored-by: SivanCola <[email protected]>
* fix(repair): reject prepared create ownership drift
Problem: A file replaced between AtomicCreateFile and ownership rebinding could be claimed by the repair transaction and removed by undo.
Root cause: The pending journal was rewritten to bind whatever state happened to be present after publication instead of proving it matched the precomputed owned state.
Fix: Keep the write-ahead ownership binding immutable and fail closed when the published path differs; add a regression test that preserves a concurrent replacement through undo.
Verification: focused crash/create tests, go test ./internal/repair, repair/fileutil race tests, Guard tests, go vet, and Windows repair test cross-compile pass.
---------
Co-authored-by: SivanCola <[email protected]>
Co-authored-by: PaulDing98 <[email protected]>
* complete stable and preview channel loop
* fix(site): harden release-channel fallbacks and version scopes
Problem:
Website CLI Stable fallbacks pointed at GitHub releases/latest asset
URLs, incomplete GitHub releases were turned into 404 download links,
and Desktop/CLI Stable races overwrote every .rxv version chip.
Root cause:
CLI releases set make_latest:false so Latest belongs to Desktop Stable;
releaseAssetMap synthesized missing CLI assets; updateStableVersion was
shared across both product surfaces.
Fix:
- CLI fallback always uses the releases page, never /releases/latest.
- Require all 7 CLI assets before accepting a release (match Worker).
- Scope package-manager .rxv updates to CLI Stable only; desktop
version chips use data-release-version="desktop".
Verification:
node --test site/src/scripts/release-channels.test.mjs
site/src/scripts/download-link.test.mjs
* fix(desktop): unify update channel controls
Problem: The desktop settings page separated channel selection, update status, and install actions across detached rows.
Root cause: The channel picker and updater state machine were rendered as independent controls, so switching channels did not read as one actionable workflow.
Fix: Merge current version/status, Stable and Preview selection, manual refresh, auto-check guidance, and state-specific download/install actions into one compact settings flow. Add the localized channel auto-check hint for English, Simplified Chinese, and Traditional Chinese.
Verification: pnpm --dir desktop/frontend run typecheck; pnpm --dir desktop/frontend run check:css; pnpm --dir desktop/frontend run test:updater (18 passed); pnpm --dir desktop/frontend run build; git diff --cached --check.
* feat(cli): persist release channel selection
Problem:
CLI users had to repeat the verbose --channel flag, and a plain upgrade always returned to Stable instead of following the user's selected public channel.
Root cause:
The native updater had no user-global channel preference, while the website and docs exposed only one-off flag syntax.
Fix:
Add a user-global [cli].update_channel setting, support upgrade stable|preview as persistent switches, retain --channel as a one-off compatibility override, reject conflicting arguments, and synchronize English/Chinese docs plus website commands.
Verification:
- env -u DEEPSEEK_API_KEY GOCACHE=/private/tmp/reasonix-go-cache go test ./...
- env GOCACHE=/private/tmp/reasonix-go-cache go vet ./internal/cli ./internal/config
- npm test (site)
- npm run build (site)
- git diff --check
* fix(release): keep CLI channel pointers monotonic
Problem: Older Stable recovery runs and out-of-order Preview publishers could overwrite a newer public CLI channel pointer.
Root cause: The release workflow wrote cli/<channel>/latest.json unconditionally and had no channel-scoped serialization or strict version comparison.
Fix: Serialize CLI releases by channel, publish immutable per-tag metadata first, validate the current pointer, and move it only when a strict Stable or Preview version tuple is newer.
Verification: bash scripts/release-workflows.test.sh; bash -n scripts/compare-cli-release-tags.sh scripts/release-workflows.test.sh; git diff --check.
* fix(config): preserve symlinked config files
Problem: Persisting the selected CLI update channel could replace a symlinked user config with a regular file.
Root cause: AtomicWriteFile renamed a temporary file over the symlink path instead of the symlink's final target.
Fix: Resolve complete config symlink chains before atomic writes and route both incremental saves and Config.WriteFile through the shared helper.
Verification: go test ./internal/config; focused writable-target and multi-level symlink regression tests; go test ./...; go vet ./internal/cli ./internal/config.
* fix(release): harden stable and preview end-to-end
Problem:
The Stable/Preview loop still allowed stale or cross-channel release pointers, unsafe Desktop release origins and retries, updater events detached from their checked target, and concurrent config writers that could lose user changes.
Root cause:
Release publication lacked one immutable candidate and strict manifest/recovery contracts; Desktop operations were not bound to request, channel, and version; config locks followed logical paths and several writers loaded stale or malformed snapshots.
Fix:
- authorize Desktop candidates from protected main-v2, separate the current control plane from recovered source commits, validate immutable assets, and make GitHub/R2 publication monotonic and resumable
- bind Desktop checks, downloads, verification, and installs to one request/channel/version state machine with strict size and manifest validation
- validate CLI, Desktop, website, and Worker release data consistently across Stable and Preview
- canonicalize and pin config targets, serialize all affected writers, preserve safe symlinks, and fail closed on malformed configuration
Verification:
- go test -race ./...; go vet ./...; go build ./...
- cd desktop && go test -race ./... && go vet ./... && go build ./...
- frontend typecheck, updater tests (31), CSS checks, and production build
- site tests (44) and production build
- Worker typecheck and tests (74)
- release workflow contracts, actionlint, cache guard, shell syntax, diff check, and Linux/Windows cross-builds
Co-authored-by: ffff2004 <[email protected]>
* fix(config): remove obsolete edit wrappers
Problem:
The release-channel hardening left two private config edit wrappers unreachable, causing the pinned golangci-lint unused check to fail.
Root cause:
Callers now resolve and pin final config targets through the transactional edit path, but the superseded convenience wrappers remained in place.
Fix:
Remove only the two unreferenced wrappers while preserving the resolved-target and timeout helpers used by production code and concurrency tests.
Verification:
- go test ./internal/config
- go vet ./...
- golangci-lint v2.12.2 run --timeout=5m
- git diff --check
* fix(release): satisfy workflow shell lint
Problem:
The final CI lint job passed golangci-lint but failed actionlint because the CLI metadata publisher assigned expected_prerelease without reading it.
Root cause:
Prerelease validation moved into validate-cli-release-manifest.sh, leaving duplicate channel-specific shell assignments in the workflow.
Fix:
Remove the unused assignments and keep prerelease validation centralized in the manifest validator.
Verification:
- actionlint v1.7.7 on all release workflows
- bash scripts/release-workflows.test.sh
- bash -n on release helper scripts
- git diff --check
* fix(release): close desktop channel blockers
Problem:
Desktop Preview-to-Stable switches failed for older Debian Stable builds, updater Wails methods broke older one-argument callers, and config edit locks could split across TMPDIR or macOS case aliases.
Root cause:
The privileged helper rejected every downgrade, strict request binding reused legacy exported method names, and lock identity depended on process-local temp directories without Darwin path folding.
Fix:
Allow only signed public Preview-to-Stable Debian downgrades, preserve legacy updater methods beside strict request-bound APIs, and use a stable hardened per-user lock registry with Darwin alias normalization.
Verification:
Root and Desktop tests, Desktop race, Go vet/build, updater 31/31, site 44/44, Worker 74/74, release workflow contracts, cache guard, actionlint, golangci-lint, frontend typecheck/CSS/build, and focused cross-process/security regressions.
---------
Co-authored-by: ffff2004 <[email protected]>
Problem: TestLoadNormalizesQuotedNodeEvalHooksPerProject flaked in CI
(test (windows-latest)) — the node -e hook child was killed at the
15.03s timeout on a loaded Windows runner, failing the job.
Root cause: hook_test.go's realSpawnTimeout (15s) feeds the per-hook
Timeout for tests that spawn real child processes. The constant's own
comment says these tests assert behavior, not latency, and node cold
starts can stall for seconds on a loaded machine — 15s just was not
generous enough for a saturated Windows GitHub runner.
Fix: raise realSpawnTimeout to 60s. The ceiling only fires on a genuine
hang, so a larger budget costs nothing when children exit normally.
Tests asserting the timeout path keep their own tight budgets (50-100ms)
and are untouched, so the suite stays fast. All real-spawn tests share
this constant (execution_contract_test.go, windows_batch_test.go,
hook_test.go), so one bump deflakes every call site.
Verification:
- go test ./internal/hook -count=1 passes locally (1.5s, node v22).
- CI on this PR exercises the windows smoke suite, which includes
internal/hook.
Problem: gating the test matrix at job level means a docs-only pull
request produces no test (ubuntu/macos/windows-latest) check runs at all
— a skipped matrix job reports only the parent job check, never the
per-leg checks the Protect main-v2 ruleset requires. Docs-only PRs then
sit at mergeStateStatus=BLOCKED with the required legs stuck in
"Expected". Confirmed live on a docs-only verification PR.
Fix: the test job now always runs (if: always()), and every step carries
the gate via a job-level RUN_STEPS env. When the changes detector
reports an unrelated diff, all steps skip and each leg reports success
in seconds, so the required per-OS checks exist and pass. When the
changes job itself fails, the legs still start and the empty output
fails open (!= 'false') into the full suite. Required check names are
unchanged; non-required jobs keep job-level skipping.
Verification:
- actionlint v1.7.7 passes; YAML parses cleanly.
- On this PR the full matrix runs (workflow files are not in any skip
list). The docs-only verification PR will be re-run after merge to
confirm legs report success and the PR turns mergeable.
* Gate CI jobs on changed paths to cut Actions waste
Problem: every pull request runs the full ci.yml matrix (11 jobs across
3 OSes) and the cache-impact guard fires on every PR event, even for
docs-only or otherwise unrelated changes. That is roughly 33k wall-minutes
per month on GitHub-hosted runners — free while the repo is public, but
pure queue time now and a real bill if the repo ever goes private.
Change:
- ci.yml: add a cheap `changes` detection job and gate the heavy jobs on
it. PRs confined to docs/, site/, release-notes/, workers/ or top-level
Markdown skip the Go test/race/lint/govulncheck/coverage jobs; PRs that
cannot affect the desktop module (which imports the root kernel via
`replace reasonix => ../`) skip the desktop/desktop-macos/desktop-windows
jobs; the site job only runs for site/ changes. Job-level `if` reports
skipped, which still satisfies the required status checks (lint, race,
test) — a workflow-level paths-ignore would leave them pending and block
merges. Detection fails open, and pushes to main-v2 always run the full
matrix.
- cache-impact.yml: paths-ignore for clearly unrelated paths so the guard
stops running on every PR event (~3k runs/month) when the PR can never
trip it.
- release-desktop.yml: retention-days: 7 on the dist-* and preview-dist
artifacts (same-run handoffs to the publish/mirror steps); they
previously kept the 90-day default, accumulating ~17 GB of dead storage.
Verification:
- actionlint v1.7.7 passes on all three modified workflows (same version
and -ignore flags as the lint job).
- Both modified YAML files parse cleanly.
- Simulated the path filter against 10 representative change sets
(docs-only, root markdown, site-only, desktop-only, internal, npm-only,
workers-only, workflow file, nested skill markdown, mixed); every output
matched the intended gating.
* Speed up pull request CI: focused race, Windows smoke, push-only installer
Problem: after the required checks, merging waits on the slowest ones —
`race` averages ~12 min and `test (windows-latest)` ~6.3 min, while the
(non-required) desktop-windows job runs ~14.5 min, most of it building the
NSIS installer. Every PR pays that cost even though main-v2 re-verifies
everything after merge.
Change:
- race: pull requests run `go test -race` only on the concurrency-heavy
packages (agent, plugin, jobs, proc, sandbox, filelock, eventwire,
remote — the same areas the job comment calls out). Pushes to main-v2
keep the full ./... sweep as the safety net. Check name is unchanged,
so the required-checks ruleset is unaffected.
- test (windows-latest): pull requests run a smoke suite of the packages
that actually carry *_windows.go code, plus cmd/. Full ./... sweep stays
on pushes. The Unix legs are untouched.
- desktop-windows: Install NSIS + installer/portable build steps now run
only on pushes; PRs stop after go test. Installer packaging is
packaging validation, and the release pipeline rebuilds it regardless.
- codeql.yml: paths-ignore docs/**, release-notes/**, root *.md on push
and pull_request (CodeQL checks are not required; the weekly cron keeps
the full scan cadence).
Expected: required-check wait drops from ~12 min to ~5 min per PR, and
the all-green wait from ~15 min to ~6 min, with no loss of post-merge
coverage.
Verification:
- actionlint v1.7.7 passes on both workflows; YAML parses cleanly.
- `go list` resolves all packages in the race subset (24 pkgs) and the
Windows smoke suite (37 pkgs).
- This PR's own checks exercise the new PR paths live: the race and
windows-test jobs run their focused variants because the branch only
touches workflow files.
Problem: every pull request runs the full ci.yml matrix (11 jobs across
3 OSes) and the cache-impact guard fires on every PR event, even for
docs-only or otherwise unrelated changes. That is roughly 33k wall-minutes
per month on GitHub-hosted runners — free while the repo is public, but
pure queue time now and a real bill if the repo ever goes private.
Change:
- ci.yml: add a cheap `changes` detection job and gate the heavy jobs on
it. PRs confined to docs/, site/, release-notes/, workers/ or top-level
Markdown skip the Go test/race/lint/govulncheck/coverage jobs; PRs that
cannot affect the desktop module (which imports the root kernel via
`replace reasonix => ../`) skip the desktop/desktop-macos/desktop-windows
jobs; the site job only runs for site/ changes. Job-level `if` reports
skipped, which still satisfies the required status checks (lint, race,
test) — a workflow-level paths-ignore would leave them pending and block
merges. Detection fails open, and pushes to main-v2 always run the full
matrix.
- cache-impact.yml: paths-ignore for clearly unrelated paths so the guard
stops running on every PR event (~3k runs/month) when the PR can never
trip it.
- release-desktop.yml: retention-days: 7 on the dist-* and preview-dist
artifacts (same-run handoffs to the publish/mirror steps); they
previously kept the 90-day default, accumulating ~17 GB of dead storage.
Verification:
- actionlint v1.7.7 passes on all three modified workflows (same version
and -ignore flags as the lint job).
- Both modified YAML files parse cleanly.
- Simulated the path filter against 10 representative change sets
(docs-only, root markdown, site-only, desktop-only, internal, npm-only,
workers-only, workflow file, nested skill markdown, mixed); every output
matched the intended gating.
* fix(desktop): allow skills in initial goal turns
Problem: Goal mode rejected or swallowed Skill invocations in the first user turn.
Root cause: The Composer blocked structured invocations before a Goal existed, while App wrapped the remaining first turn as /goal prose.
Fix: Require task text but preserve selected and pasted Skill invocations as structured submissions after activating the Goal. Extract the activation/submission order into a tested contract and add coverage for inline and subagent-compatible invocation paths.
Verification: pnpm exec tsx src/__tests__/composer-goal-toggle.test.tsx; pnpm exec tsx src/__tests__/send-failed.test.ts; pnpm typecheck; pnpm exec tsc --noEmit -p tsconfig.test.json; ./scripts/cache-guard.sh
* fix(desktop): scope initial Goal activation to source tab
Problem: Switching tabs while the first Goal was activating could store the Goal on the newly active tab while submitting the structured Skill to the original tab. Initial Goal setup also rejected attachment- or workspace-reference-only prompts after the Skill guard changed.
Root cause: The activation callback closed over activeTabId, and the Composer guard treated every empty-text first Goal as invalid even when non-text context was present.
Fix: Add explicit tab-scoped Goal/profile activation, route the initial structured submission through the source tab, preserve attachment and workspace-reference-only submissions, and cover the active-Goal Subagent path plus source-tab wiring.
Verification: go test ./internal/control -count=1; go test -race ./internal/control -count=1; go test -race ./internal/jobs -run TestStartForSession_AcceptsValidInput -count=1; pnpm exec tsx src/__tests__/composer-goal-toggle.test.tsx; pnpm exec tsx src/__tests__/send-failed.test.ts; pnpm typecheck; ./scripts/cache-guard.sh; git diff --check.
Co-authored-by: SivanCola <[email protected]>
* fix(desktop): fail closed when initial Goal activation fails
Problem: Structured Skill submission on the first Goal turn could run even when SetGoalForTab failed or no-oped, because activation errors were swallowed and the structured path has no /goal prose fallback.
Root cause: setGoalForTab used .catch(() => {}), applyGoalForTab patched the local Goal profile before backend success, and regression coverage only asserted source wiring instead of deferred/bridge-failure behavior.
Fix: Propagate SetGoalForTab failures (still refresh meta in finally), patch the composer Goal profile only after backend activation succeeds, and document that activateGoalAndSubmit must abort send when activation fails. Add behavioral tests for bridge failure, deferred source-tab ordering, and attachment-/workspace-ref-only initial Goals.
Verification: pnpm exec tsx src/__tests__/send-failed.test.ts; pnpm exec tsx src/__tests__/composer-goal-toggle.test.tsx; pnpm typecheck.
* fix(desktop): return SetGoalForTab errors across the Wails boundary
Problem: Frontend fail-closed Goal activation still continued structured Skill submits when the real bridge "succeeded", because Go SetGoalForTab returned void and remote/invalid-tab failures only warned.
Root cause: The Wails method swallowed remote RPC errors and missing/replaced tabs with a normal return, so the Promise always resolved. Cross-tab coverage also only exercised helper callback order, not SetGoalForTab/SubmitInvocationsToTab tab ids through the controller bridge.
Fix: Make SetGoal/SetGoalForTab/ClearGoal/ClearGoalForTab return error (remote RPC, missing tab, replaced tab). Route the first Goal turn through activateGoalAndSubmitOnTab so source tab ids are captured once. Add desktop Go error tests and a controller bridge harness that hangs SetGoalForTab(A), switches to B, and asserts Goal plus SubmitInvocationsToTab stay on A.
Verification: cd desktop && go test -count=1 -run 'TestSetGoalForTab|TestClearGoalForTab|TestMetaReportsGoalStatus|TestCollaborationModesPreserveToolApprovalMode|TestToolApprovalModesPreserveCollaborationMode' .; pnpm --dir desktop/frontend exec tsx src/__tests__/goal-activation-tab-routing.test.tsx; pnpm --dir desktop/frontend exec tsx src/__tests__/send-failed.test.ts; pnpm --dir desktop/frontend typecheck.
* fix(desktop): handle Goal bridge rejections
Problem: Interactive and background Goal calls discarded rejected Wails promises, allowing unhandledrejection to reach the global crash overlay.
Root cause: Composer callbacks, mode shortcuts, model commands, and controller resync paths invoked asynchronous Goal actions without a shared error boundary.
Fix: Route non-submit Goal actions through a toast-aware rejection handler, await model commands, and defer local Goal state updates until bridge success while preserving fail-closed structured submissions.
Verification: pnpm frontend test/typechecks, focused Goal routing and rejection tests, git diff --check, and cache-guard.sh.
* fix(desktop): fence initial Goal submission to its target
Problem:
An initial Goal could activate on Remote tab A, then submit its structured Skill to Local tab A after the user switched projections. The routing regression also was not part of the standard frontend test suite.
Root cause:
Goal activation and the first turn crossed Wails as separate calls identified only by tab ID. Wails schedules calls independently, while Local and Remote projections can reuse the same tab ID.
Fix:
Capture the source tab and workbench target token in React, validate both in one backend operation, and hold the workbench transition lock across Goal activation and first-turn submission. Fail closed on stale targets, keep zero-valued request sequences in target events, and cover frontend bridge plus Go backend behavior.
Verification:
- pnpm --dir desktop/frontend test
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend test:typecheck
- cd desktop && go test ./...
- cd desktop && go test -race -count=1 -run 'TestSubmitInitialGoal|TestWorkbenchTargetEventKeepsZeroRequestSequence|TestSetGoalForTab|TestClearGoalForTab|TestSetActiveTabSwitchesRemoteProjectionOnlyForDifferentTab' .
- cd desktop && go vet ./...
- ./scripts/cache-guard.sh
* feat(desktop): add integrated terminal sessions
Add a right-dock xterm terminal with multi-session controls, workspace-relative start directories, and file-tree entry points. Use Unix PTY and Windows ConPTY backends with bounded session counts, output buffering, stale-tab checks, read-only and remote guards, and application shutdown cleanup. Keep shell resolution renderer-safe by accepting only backend-approved shell IDs and reading custom shell paths from user-global config.
Refs #6513
Refs #6891
Verification: desktop Go race tests, Windows amd64 cross-compile, frontend terminal tests, full frontend test suite, typecheck, CSS checks, and production build.
Co-authored-by: ipanta <[email protected]>
* feat(desktop): scope terminal to each chat tab
Align the integrated terminal with the chat-scoped Codex workflow. Bind sessions to the active chat tab and workspace, close only that tab's PTYs on tab close, and render the terminal as a responsive bottom drawer.
Expose a bounded terminal output snapshot through an explicit user action that adds it to the composer. Do not inject terminal output into provider prompts or bypass the existing agent shell approval path.
Verification: desktop terminal race tests, Windows amd64 cross-compile, frontend typecheck, terminal and workspace-layout tests, CSS checks, and production build.
Co-authored-by: ipanta <[email protected]>
* fix(desktop): harden integrated terminal lifecycle
Problem:
- Terminal output was lost after switching sessions or reopening the drawer, and concurrent session creation could overwrite renderer state.
- Closing a chat tab could race an in-flight terminal start, while Windows coverage did not exercise the real ConPTY path.
- The terminal drawer duplicated controls and loaded xterm into the initial application bundle.
Root cause:
- Output was buffered only while no sink was attached, state updates reused stale workspace snapshots, and the backend did not remember closed tab IDs.
- Terminal-specific frontend tests were outside the standard suite, and the Windows executable fixture lacked an .exe suffix.
Fix:
- Keep bounded per-session output history, clear it only when a session is removed, and merge concurrent session updates atomically.
- Tombstone closed tabs before process cleanup, reject late starts, suppress stale output, and distinguish natural exits from removals.
- Add backend-approved shell selection, compact horizontal session tabs, lazy terminal loading, Windows ConPTY coverage, and CI wiring.
Verification:
- Desktop terminal race tests, full desktop tests, and go vet pass.
- Windows amd64 test-binary cross-compilation passes.
- Full frontend tests, TypeScript type checking, and production bundle checks pass.
* fix(desktop): close integrated terminal review gaps
Problem:
- Read-only transitions and workspace rebinding could leave existing shells alive or allow a slow shell start to register against stale tab state.
- First-open, retry, concurrent create, and single-session UI states could lose errors, duplicate requests, or hide the only close control.
- Terminal output added to chat could retain terminal control payloads or break its Markdown fence.
Root cause:
- Terminal lifecycle invalidation was not serialized with tab capability changes, and process exit did not wait for the output reader to drain.
- Renderer readiness used replace-style state updates without a retryable error model.
- Composer insertion treated raw terminal output as ordinary text.
Fix:
- Add per-tab generations, capability gates, process-output draining, process-group cleanup coverage, and macOS PTY CI.
- Make terminal readiness deduplicated and retryable, preserve concurrent sessions, expose operation errors, and keep bottom-drawer controls usable in all layouts.
- Strip ANSI, OSC, C1, and unsafe control sequences, bound inserted output, and choose a safe Markdown fence.
Verification:
- go test ./... and go vet ./... in desktop/
- go test -race -run 'Test(ResolveTerminal|Terminal|EmptyTerminal|UnixTerminalProcess)' . in desktop/
- Windows amd64 and arm64 cross-builds for desktop/
- pnpm test, test:typecheck, test:remote, test:updater, test:performance, typecheck, test:terminal, and build
- go vet ./..., gofmt, YAML parse, and git diff --check
---------
Co-authored-by: ipanta <[email protected]>
* feat(memory): separate standing instructions from scoped facts
* fix(memory): preserve scoped memory compatibility
Problem:
Omitted-scope updates could move legacy global memories into the current project, and global preferences or feedback were unavailable on the first turn. Mixed-version clients could also route independently scoped facts through the wrong directory.
Root cause:
Save normalized an empty scope before consulting the active file, the stable prompt only contained the memory index, and previous releases treated metadata.type as the routing signal.
Fix:
Preserve the effective scope of same-name updates, snapshot global user and feedback bodies into deterministic lower-priority guidance, retain a previous-release routing type alongside authoritative fact_type metadata, and queue complete save/revocation notes for the current session.
Verification:
./scripts/cache-guard.sh
go vet ./...
env -u DEEPSEEK_API_KEY go test ./...
(cd desktop) go test ./...
(cd desktop/frontend) pnpm test
(cd desktop/frontend) pnpm build
gofmt -l .
git diff --check
* refactor(context): separate raw and provider user input
* refactor(instructions): resolve scoped context with provenance
* refactor(memory): add versioned fact storage
Context: Memory facts were addressed by mutable slugs, trusted a separately maintained index, and had no durable update history or conflict protection.
Change: Add stable IDs, monotonic revisions, atomic writes, derived indexes, deterministic legacy migration, scoped identity handling, revision restore, and optimistic update checks. Surface the metadata through tools and Desktop while preserving legacy frontmatter routing and Safe Mode behavior.
Compatibility: Existing files remain readable, gain deterministic runtime metadata before migration, and keep metadata.type/fact_type/scope values that previous releases understand. Added Desktop JSON fields are optional.
Verification: go test ./internal/memory; go test -race ./internal/memory; go test ./internal/boot -run Test.*Memory; go test ./... with provider credentials isolated; desktop go test ./...; frontend pnpm test and typecheck; go vet ./...; ./scripts/cache-guard.sh.
* feat(memory): recall relevant facts per user turn
Problem:
Saved facts were only visible through a stable index and required the model or user to invoke memory search manually.
Design:
Add conservative host-side BM25 recall for real user turns with project-over-global deduplication, freshness labels, explainable scoring, strict character limits, and local-home redaction. Keep the system prompt and tool schemas stable by appending recall only to the provider user-turn tail, and exclude synthetic turns and already-loaded global guidance.
Compatibility:
Preserve Store.List behavior and add ListAll for scope-aware consumers. No persisted memory or session fields change; display fallbacks strip the new transient suffix.
Verification:
go test -count=1 ./internal/memory ./internal/agent ./internal/control
go test -race -count=1 ./internal/memory
go test -race -count=1 ./internal/control with focused recall tests
go test -count=1 ./internal/tool/builtin/ ./internal/boot/
go vet ./...
./scripts/cache-guard.sh
* feat(memory): auto-save low-risk project facts
Problem:
Every remember call required a fresh approval, including bounded project facts that were new, non-sensitive, and low impact. This made durable context expensive enough that agents and users often skipped it.
Design:
Classify interactive remember requests against the scoped store. Automatically allow only explicit project/reference creates that are bounded, non-sensitive, and not semantic duplicates. Keep global, user/feedback, sensitive, and overwrite requests on the existing fresh-human path; keep headless and sub-agent mutation fail closed.
Safety:
Issue one-shot hashed grants and enforce create-only again inside the serialized Store write, preventing an approval-to-write race from becoming an overwrite. Clear abandoned grants at turn completion. Expose revision history and audited restore through the controller.
Compatibility:
The remember description and schema are unchanged. RequireCreate is a runtime-only SaveOptions field; persisted memory and session formats are unchanged.
Verification:
DEEPSEEK_API_KEY= go test -count=1 ./...
go test -count=1 ./... in desktop
go test -race -count=1 ./internal/memory ./internal/control with memory/approval tests
go vet ./...
./scripts/cache-guard.sh
* feat(memory): recover archived facts safely
Problem: Archived facts remained inspectable on disk but users could not recover them through Reasonix, and ad hoc restoration could overwrite active memory or lose revision provenance.
Fix: Add store-validated archive recovery with collision checks, exclusive active-file creation, monotonic revisions, revision snapshots, controller turn-tail refresh, and a Desktop restore action.
Compatibility: Existing active and archived Markdown files remain readable; recovery writes the established v2 frontmatter and hidden revision layout, while older binaries continue to read the restored active fact.
Verification: go test ./internal/memory ./internal/control; go test ./... in desktop; pnpm test; pnpm exec tsc --noEmit; ./scripts/cache-guard.sh.
* feat(context): expose memory recall diagnostics
Expose scoped fact freshness, project-over-global conflict decisions, and the last automatic recall trace through the desktop contract. Add revision history and restore APIs while preserving non-null collection responses and remote-workspace isolation.
This gives Context Center enough provenance to explain what was available, selected, suppressed, or restored without changing the stable prompt prefix.
* feat(context): add provenance center to desktop
Make scoped memories explainable in the desktop settings surface. Show stable identity, freshness, revision history, project-over-global resolution, automatic recall traces, instruction precedence, imports, and diagnostics.
The suggestions workflow remains compatible while the Context Center gains zero-impact read paths and explicit restore actions for user-controlled recovery.
* feat(memory): make suggestions zero-config
Open the suggestions surface with automatic local-history scanning by default, removing the hidden localStorage setup switch. Keep every candidate behind explicit confirmation.
Deduplicate against all scoped active facts and resolved instruction bodies so project and global memories cannot produce repeated guidance.
* feat(memory): add CLI diagnostics and recovery
Context:
Context Engine v2 exposed revision, recall, and instruction provenance in the backend and desktop, but the CLI only listed legacy merged facts and archives.
Changes:
- add shared /memory diagnostics for scoped facts, recall traces, instruction precedence, revisions, and archives
- add explicit revision restore and archive recovery commands with structured completion across CLI, desktop, and remote workspaces
- preserve same-name facts across scopes and keep narrow TUI output bounded
Verification:
- go test ./internal/control ./internal/cli ./internal/remote/workbench/runtime ./internal/memory -count=1
- go test -race ./internal/control ./internal/memory
- cd desktop && go test . -count=1
- ./scripts/cache-guard.sh
Cache-impact: management diagnostics only; provider-visible prompt and tool schemas are unchanged.
* docs(context): document instructions and memory v2
Context:
Context Engine v2 changed instruction precedence, automatic recall, safe memory writes, revision recovery, and desktop suggestions, while the user docs still described the legacy approval-only retrieval model.
Changes:
- rewrite the context and memory contract around standing instructions versus background facts
- document recall freshness, project-over-global resolution, zero-configuration writes and suggestions, revision/archive recovery, cache stability, privacy, and remote fail-closed behavior
- add a complete Chinese companion and update Guide, CLI, migration, spec, approval, and README navigation in both languages
Verification:
- validated relative Markdown links, code fences, and final newlines across 14 changed files
- scanned for stale approval/suggestion claims
- git diff --cached --check
- scanned the staged diff for secrets and machine-local paths
* fix(context): preserve safe headless memory writes
Allow controller-scoped low-risk project/reference creates to pass the non-interactive approval gate while keeping deny rules and all higher-risk memory operations fail-closed. Reject archive recovery through symlinked files or archive directories, with end-to-end regressions for both boundaries.
* fix(memory): reject non-directory migration roots
* fix(context): make instruction and memory references safe
Problem:
Provider-visible instruction paths exposed machine-local roots, memory results exposed store paths, and listed scoped references could not be used directly for read, update, revision, or archive operations.
Root cause:
Instruction rendering used absolute source paths and memory lookup treated display names as unqualified slugs. Symlink reads also crossed boundaries through a path-resolution gap, while scope-aware listing deduplicated same-name facts.
Fix:
Render stable workspace/user provenance, confine instruction and import reads to their owning roots, reject external symlinks, and support project/<name>.md and global/<name>.md references end to end. Preserve unqualified compatibility, stable-ID renames, revision snapshots, exact-scope archive behavior, low-risk create-only approval boundaries, and bilingual documentation.
Verification:
Focused instruction/memory tests and race tests; go test -count=1 ./...; go test -race ./...; go vet ./...; desktop go test/race/vet; pnpm test:all; pnpm build; go run ./cmd/remote-protocol-gen --check; ./scripts/cache-guard.sh (8/8 pass, long tool loop tail average 95%); staged privacy scan clean.
* docs(contributors): attribute adopted context work
Attribution:
Record the authors whose prior pull request mechanisms were adapted by the Context Engine v2 integration.
- cyq1017: feedback injection from #5710 and Markdown input normalization from #5603
- salarkhannn: Markdown input normalization from #5604
Scope:
This metadata-only commit adds repository-level co-author attribution without changing runtime behavior.
Verification:
Confirmed source PR ownership and both public GitHub noreply identities.
Co-authored-by: cyq <[email protected]>
Co-authored-by: Salar <[email protected]>
* test(desktop): allow slow Windows git metadata probe
Problem:
The Windows Desktop CI job failed when MetaForTab's asynchronous first git probe did not finish within the test's one-second deadline.
Root cause:
The production path intentionally launches the cold git branch lookup in the background so tab metadata never blocks. Windows runner process startup can exceed one second under normal CI load, making the test timing assumption platform-fragile.
Fix:
Keep the non-blocking metadata implementation unchanged and extend the cross-platform test observation window to five seconds, with a comment documenting the Windows process-startup variance.
Verification:
- GOCACHE=/tmp/reasonix-desktop-go-cache go test -count=1 -run 'TestMetaForTabIncludesWorkspaceContext|TestWorkspaceGitBranch' ./desktop
- git diff --check
---------
Co-authored-by: cyq <[email protected]>
Co-authored-by: Salar <[email protected]>
* fix(cli): fall through keyless default_model to next configured provider
The CLI built sessions on whatever cfg.DefaultModel said, even when that
provider had no API key in the current environment. Every command
(
easonix -p,
easonix run,
easonix review,
easonix acp,
subagent runs) then hard-failed with 'missing env X_API_KEY' until the
user manually picked a different model in the bottom switcher, even
when another configured provider was sitting right there (#6996).
A companion fix to desktopNewSessionModel in #6999, which only covered
the desktop NewSession path. This change covers the shared boot.Build
entry point used by every CLI subcommand plus the six pre-Build call
sites that resolve the bootstrap / banner / effort / ACP model ref.
Semantics:
- No explicit --model: use cfg.DefaultModel when configured, else
fall through to the first provider that has a configured key. When
nothing is configured the raw default is preserved so the existing
'unknown model' / 'missing env' messages still surface.
- Explicit --model (or session-level p.Model / f.model): strict
resolve, fail loud on keyless or unknown. We must never silently
reroute a model the user asked for by name.
Verification:
- go test -count=1 ./internal/cli/... ./internal/boot/... ./internal/config/...
- go vet ./...
- gofmt -l internal/cli/ internal/boot/
- manual: a config with keyless default_model + a configured second
provider now boots onto the second provider (the prior build
hard-failed on the keyless one).
- manual: --model <keyless_ref> still errors; --model <configured_ref>
still works.
Cache-impact: none - CLI/boot model selection only, no provider-visible
prompt, tool schema, memory prefix, or cache key touched.
Cache-guard: TestBuildKeylessDefaultFallsBackToConfiguredProvider +
TestBuildExplicitKeylessModelStillFails in internal/boot, plus the
8-case TestResolveModelForCLI table in internal/cli/default_model_test.
System-prompt-review: N/A.
* fix(cli): preserve explicit model in TUI state
Keep the controller's resolved model reference as the TUI source of truth so interactive keyless recovery and later controller rebuilds never reinterpret an explicit model as an implicit default. Add a regression covering an explicit keyless model with the interactive RequireKey=false path.\n\nCo-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* fix(agent): stop stale edit retry loops
Problem:
Goal mode could alternate successful file reads with repeated stale edit_file or multi_edit calls, bypassing the existing consecutive-failure guard and wasting automatic continuation turns.
Root cause:
The host treated any successful read as progress and had no goal-scoped, target-aware memory of failed write intents.
Fix:
Track semantic failed-write signatures across one user turn or stable Goal scope, invalidate them only for overlapping mutations, and re-check stale-anchor applicability through the existing side-effect-free preview before blocking a retry.
Verification:
- go test ./internal/agent -run 'TestRepeatGuard' -count=1
- go test -race ./internal/agent -run 'TestRepeatGuard' -count=1
- env -u DEEPSEEK_API_KEY go test ./... -count=1
- ./scripts/cache-guard.sh
* fix(agent): retain stale edit failures across file changes
Problem:
Repeated stale anchor failures could regain retry budget after another successful edit to the same file or after changing the target path spelling.
Root cause:
The Goal-scoped guard cleared every overlapping failure record, keyed signatures with raw paths, and treated dynamic nearest-line diagnostics as the error class.
Fix:
Preserve Preview-recheckable anchor failures across mutations, normalize anchor error classes and target paths, and cover real edit_file state changes plus path aliases.
Verification:
- go test ./internal/agent -run 'TestRepeatGuard' -count=1
- go test -race ./internal/agent -run 'TestRepeatGuard' -count=1
- env -u DEEPSEEK_API_KEY go test ./... -count=1
- go vet ./...
- ./scripts/cache-guard.sh
* fix(agent): reset recoverable failures between goal runs
Problem:
Ordinary write failures were retained across Run calls in the same Goal scope, so a recovered permission or external-state issue could be blocked before retry execution.
Root cause:
The Goal-scoped repeat-failure map preserved every write-like failure even though only stale-anchor failures have a side-effect-free state recheck.
Fix:
Keep stale-anchor failures across Runs and discard ordinary write failures at the next Run boundary. Add a regression test proving the write executes again in each Run.
Verification:
go test ./internal/agent -run 'TestRepeatGuard' -count=1
go test -race ./internal/agent -run 'TestRepeatGuard' -count=1
go test ./internal/control -run 'TestRecoveryPauseKeepsGoalRunningAndDeliveryScope|TestGoalReadinessFailureBlocksAndKeepsDeliveryScope' -count=1
go vet ./...
env -u DEEPSEEK_API_KEY go test ./... -count=1
./scripts/cache-guard.sh
* fix(agent): tailor repeated write failure guidance
Problem:
The repeat-failure guard used stale-anchor recovery instructions for every repeated write failure, including permission and filesystem errors.
Root cause:
The block message did not distinguish Preview-recheckable anchor failures from ordinary write failures even though the failure record already carried that classification.
Fix:
Keep anchor-specific guidance for stale edit failures and return generic write-condition guidance for ordinary failures. Extend the permission-denied regression test to reject stale-anchor wording.
Verification:
go test ./internal/agent -run 'TestRepeatGuard' -count=20
go test -race ./internal/agent -run 'TestRepeatGuard' -count=1
go test ./internal/control -run 'TestRecoveryPauseKeepsGoalRunningAndDeliveryScope|TestGoalReadinessFailureBlocksAndKeepsDeliveryScope' -count=10
go test ./internal/agent -count=1
GOCACHE=/private/tmp/reasonix-go-cache-vet2 go vet ./internal/agent
./scripts/cache-guard.sh
* refactor(agent): add TaskTool Options constructor / 增加 TaskTool Options 构造入口
Problem:
NewTaskTool takes 18 positional parameters, which is hard to extend and
easy to mis-order at call sites.
Root cause:
Construction was only exposed as a long positional helper with no
structured options type.
Fix:
- Add TaskToolOptions and NewTaskToolWithOptions as the internal standard
entry point (empty SysPrompt still maps to DefaultTaskSystemPrompt).
- Keep the 18-arg NewTaskTool as a compatibility wrapper for one full
iteration cycle.
- Migrate boot wiring and the shared test helper to Options.
- Add equivalence tests for config, schema, empty prompt, resolver, and
execute behavior.
Verification:
go test ./internal/agent -run 'TestNewTaskToolWithOptions|TestTaskTool' -count=1
go test ./internal/boot -count=1
* refactor(agent): split Run and executeOne state machines / 拆分 Run 与 executeOne 状态机
Problem:
Agent.Run and executeOne mixed lifecycle, turn policy, and tool policy in
two very large methods, raising cognitive load without changing behavior.
Root cause:
Turn-loop counters and tool-call gating lived inline instead of package-
private staged helpers with a single ownership model for locks/defers.
Fix:
- Keep Run as the lifecycle owner of workspace lease, evidence commit,
delivery checkpoint, steer queue, and active-turn cleanup.
- Extract runLoopState plus beginRunTurn, runToolLoop, handleFinalResponse,
and handleToolRound without changing stream/persist/batch/compact order.
- Split executeOne into parseToolCall, resolveToolPolicy (plan/proxy,
delivery, recovery/permission), prepareToolExecution, and
finishToolExecution behind toolCallPlan, preserving permission-before-
lease and PreToolUse-before-execute semantics.
Verification:
go test ./internal/agent/... -count=1
go test -race ./internal/agent/... -count=1
REASONIX_RELEASE_CACHE_GUARD=1 go test ./internal/agent -run '^TestReleaseCacheHitGuard$' -count=1
* ci: report-only agent complexity monitor / 增加 agent 复杂度报告模式监控
Problem:
The repo already runs gofmt, vet, build, full tests, race, and
golangci-lint, but had no trend signal for remaining internal/agent
hotspots after structured extraction.
Root cause:
Main lint intentionally avoids funlen/cyclop hard fails that would
generate a flood of pre-existing noise.
Fix:
- Add a report-only funlen+cyclop config scoped to internal/agent
production code with generous thresholds (not a hard 50-line rule).
- Run it beside the existing lint job with issues-exit-code=0, write a
CI summary, and upload the report as an artifact.
- Ignore the generated local report file.
Verification:
bash scripts/agent-complexity-report.sh
* test(provider): isolate live DeepSeek cache probe / 隔离 DeepSeek 真实缓存探针
Problem:
The environment-gated DeepSeek cache probe ran during ordinary test suites whenever a stale API key was present, and its diagnostics still claimed tool-call reasoning replay should have zero prompt-token cost.
Root cause:
The probe had no explicit live build tag and retained commentary from the obsolete policy that stripped reasoning_content from DeepSeek tool-call turns.
Fix:
Gate the probe behind the live build tag, document the tagged invocation, describe required replay versus the empty-key recovery fallback, and fail when the required replay request is rejected.
Verification:
- go test ./...
- go test -race ./internal/provider/openai -count=1
- env -u DEEPSEEK_API_KEY go test -tags live ./internal/provider/openai -run TestRealDeepSeekCacheProbe -count=1 -v
- go vet ./internal/provider/openai
- ./scripts/cache-guard.sh
* fix(desktop): prevent freezes in large sessions
Problem:
Large session collections, rapid profile/model changes, and long streaming responses could trigger repeated filesystem scans, redundant controller rebuilds, and expensive transcript-wide rerenders. Remote Profile and Goal persistence could also diverge if the process stopped between durable writes.
Root cause:
Project-tree refreshes invalidated a global cache, profile axes crossed independent asynchronous calls, model changes queued every intermediate selection, and streaming Markdown reparsed the growing document on every delta. Remote session registry and Goal sidecar updates had no recovery journal.
Fix:
Add directory-scoped cache generations with TTL reconciliation, serialized atomic composer profiles, coalesced model switching, event-driven tab metadata refresh, an external live-stream store, batched stable Markdown rendering, lazy frontend chunks, and bundle/performance budgets. Preserve empty-history Goal state and make remote Profile plus Goal updates crash-recoverable with a durable transaction journal.
Verification:
- go test -race ./... in root and desktop modules
- focused cache, Profile, Goal, and model lifecycle race tests repeated 10 times
- pnpm test:all and production frontend build
- go vet ./... in root and desktop modules
- protocol generation drift and cache guard checks
- Windows amd64 and Linux amd64 cross-compilation checks
* fix(desktop): eliminate metadata and task rollback races
Problem:
Concurrent AutoResearch task creation could select the same directory ID across Store instances, and a failed durable Goal rollback could remove another transaction's task. Tab metadata refreshes could also apply a snapshot begun before a tab mutation.
Root cause:
Task IDs were chosen by a check-then-create sequence and deletion had no ownership proof. The bounded tab refresh coordinator treated a saturated in-flight request as current even after a backend mutation.
Fix:
Reserve task directories with atomic Mkdir and attach an opaque creation token required for rollback deletion. Add mutation generations and a latest-only trailing refresh to the tab metadata coordinator, then use it for lifecycle and explicit tab mutations.
Verification:
- go test ./internal/autoresearch
- go test -race ./internal/autoresearch
- go test -race ./internal/control ./internal/remote/workbench/runtime
- go test ./... (desktop module)
- pnpm --dir desktop/frontend test:all
- go vet ./internal/autoresearch ./internal/control ./internal/remote/workbench/runtime
* fix(remote): recover AutoResearch profile side effects
Problem:
Prepared Profile+Goal recovery restored the session registry and Goal sidecar but could leave a newly-created AutoResearch task running after a crash.
Root cause:
The task ownership token was generated inside task creation and was not durable in the parent transaction before the task directory appeared.
Fix:
Persist a transaction-owned create token in the prepared journal, bind transaction task IDs to a hash of that token, and remove the uniquely-owned task during prepared rollback and restart recovery. Preserve committed tasks and cover incomplete directory reservations.
Verification:
- go test ./internal/autoresearch ./internal/control ./internal/remote/workbench/runtime
- go test -race ./internal/autoresearch ./internal/control ./internal/remote/workbench/runtime
- go vet ./internal/autoresearch ./internal/control ./internal/remote/workbench/runtime
* ci: retry final baseline checks
Desktop (Wails):
- MarkdownRenderer annotates .katex/.katex-display/.katex-html with
data-latex-source attribute from rehype-katex MathML annotation
- messageSelectionCopy handler reads data-latex-source and replaces
KaTeX rendered text with the original $...$ / ... LaTeX markup
- Auto-locks to the full formula when any part is selected
- Collects all display blocks in the range for multi-row formulas
CLI (TUI):
- mathNode retains both value (Unicode display) and source (raw LaTeX)
- mdRenderer gains rawMath mode and RenderRaw() method
- renderAssistantMarkdownRaw builds a parallel transcript with
raw LaTeX preserving $...$ delimiters
- selectedText() uses rawWrappedLines when available to restore
original LaTeX on clipboard copy
Fixes: Ctrl+C copy of LaTeX math expressions in both desktop and CLI
* fix(desktop): clear stale session data when reusing blank tab / 修复新建会话时展示旧工具调用记录的问题
When creating a new session tab via handleNewTab → openBlankSession → ensureBlankTab, the backend may reuse an existing blank tab that still carries stale session data (e.g. tool calls from a previous session). The frontend then loads this stale data via HistoryPageForTab, rendering 'N 个工具' tool-fold cards that should not appear in a fresh session.
The fix calls app.NewSessionForTab(meta.id) after EnsureBlankTab returns, mirroring what newSession() already does, to guarantee the backend session is clean before loadSessionDataForTab runs. A silent catch with addBreadcrumb preserves diagnostic signal when the backend refuses.
Cache-impact: none — only desktop/frontend TypeScript, no cache-sensitive internal paths.
* Fix reused blank tab hydration
Problem:
Reusing an existing blank tab could leave stale tool cards visible and allow an earlier history request to repopulate them.
Root cause:
The frontend kept the existing tab state and used a non-reset hydration. The added NewSessionForTab call did not invalidate local requests and could no-op for an already blank backend session.
Fix:
Remove the redundant backend session rotation, invalidate checkpoint work, and force a reset hydration bound to the TabMeta session path.
Verification:
- pnpm exec tsx src/__tests__/new-session-load-race.test.tsx
- pnpm exec tsc --noEmit
- cd desktop && go test ./...
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* Add reused blank tab hydration regression
Contract:
A reused blank tab starts a fresh history load, clears orphaned tool cards immediately, and rejects late history from the previous session.
Coverage:
Add an integration-style useController test that holds the startup history request, reuses the same tab id with a new session path, and releases the stale tool history after the fresh empty hydration completes.
Verification:
- pnpm exec tsx src/__tests__/new-session-load-race.test.tsx
- pnpm exec tsc --noEmit
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* Fix stale desktop navigation completions
Problem:
A tab click made while blank, topic, or session navigation was in flight could be overwritten by the older completion. The frontend could show an abandoned tab or disagree with the backend active tab.
Root cause:
Navigation and tab-switch queues tracked last-click ownership independently, and controller navigation helpers advanced their epoch only when execution began. A queued click therefore did not invalidate an already-running operation, while late metadata and hydration work could still apply.
Fix:
Capture one shared navigation intent at enqueue time, propagate it through composite controller operations, and reject stale UI, hydration, and metadata updates. When a stale backend navigation already activated its result, reassert the currently visible tab so frontend and backend focus converge.
Verification:
- new-session-load-race: 28 passed
- tab-switch-hydration: 75 passed
- activate-topic-stale: 8 passed
- open-topic-coalescing: 15 passed
- app-chrome-tabs: 83 passed
- pnpm exec tsc --noEmit
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* Fix stale navigation reassertion races
Problem:
A stale tab switch or fork repair could finish after a third navigation and leave the backend active tab behind the visible frontend tab.
Root cause:
Both stale-completion paths captured the visible tab once and issued a single SetActiveTab call without rechecking ownership after the RPC returned.
Fix:
Route stale switch and fork completions through the shared reassert loop so each await is followed by a fresh visible-tab check.
Verification:
- pnpm exec tsx src/__tests__/tab-switch-hydration.test.tsx
- focused desktop frontend navigation matrix
- pnpm exec tsc --noEmit
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* Add stale navigation repair regressions
Contract:
A stale tab-switch or fork repair must keep following navigation ownership when a third navigation completes while SetActiveTab is still pending.
Coverage:
Use deferred backend gates to force both three-way interleavings and assert that the latest tab remains visible and backend-active.
Verification:
- pnpm exec tsx src/__tests__/tab-switch-hydration.test.tsx (79 passed)
- focused desktop frontend navigation matrix (409 passed)
- pnpm exec tsc --noEmit
- git diff --check
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: xiaoshuai1024 <[email protected]>
Co-authored-by: Sivan <[email protected]>
Session rebind (project-tree switch) rebuilds the controller with a fresh
executor whose LastUsage() returns nil and ContextSnapshot() reports used=0.
The telemetry file (.telemetry.json) already had cumulative totals but was
missing three categories of data needed after a rebind:
1. Context fill (used): now persisted as LastUsedTokens in sessionUsageStats
and used as a fallback in ContextUsageForTab/ContextPanel when the executor
reports used=0.
2. Per-turn token breakdown (prompt/completion/reasoning/cache hit/miss): now
persisted as Last* fields and restored in ContextPanel when LastUsage() is
nil, keeping the donut chart and type breakdown accurate.
3. Right-panel refresh timing: a new contextPanelSeq counter is bumped after
hydration ancillary data lands, merged into ContextPanel's refreshKey so
RequestCount/ElapsedMs/SessionCost re-fetch after a rebind instead of
showing stale data.
Frontend also drops the stillVisible guard on ancillary context/effort/jobs
dispatch (tab-scoped dispatchTo is safe for non-visible tabs) and adds an
activeTabId-driven fallback effect for StatusBar.
Fixes: #5335, #5766
* fix(jobs): reject traversal in SetActiveSessionPath sessionPath
Follow-up to the issue #6932 thread (the StartForSession half is fixed in
#6936). SetActiveSessionPath trimmed sessionPath but did not reject `..`
components. Combined with store.SessionJobsDir(sessionPath) (which builds
<id>.jobs from the transcript path), a malicious or malformed sessionPath
like "[tmp][..][sibling][passwd.jsonl]" would resolve to a sibling
sidecar directory and then flow into:
* m.artifactDirs[parentSession] = newDir
* migrateArtifactDirForSession (file copies to the caller-picked dir)
* loadSessionArtifacts (reads meta files from the caller-picked dir)
This change:
* Adds validateSessionPath, which rejects empty input, NUL and control
characters, and any path component equal to "..". Unlike
validatePathSegment, separators are allowed because sessionPath is an
absolute or workspace-relative path. Names like "..hidden" and "..."
remain accepted as legitimate filenames; only ".." as a standalone
component is rejected.
* Calls validateSessionPath at SetActiveSessionPath entry, after trim
and before any lock or filesystem side effect. A failure emits a
Warning notice through the manager's sink (so the caller can observe
the rejection in its event stream) and returns immediately without
touching m.active, m.artifactDirs, or the migration/load paths.
* Adds jobs_sessionpath_test.go with a 15-case table for
validateSessionPath, plus three integration tests:
- TestSetActiveSessionPath_RejectsTraversalPayload: a payload
containing ".." does not create a sidecar directory next to a
canary file, leaves m.artifactDirs empty, and emits the expected
warning.
- TestSetActiveSessionPath_RejectsLeadingDotDot: a leading ".."
component is rejected the same way.
- TestSetActiveSessionPath_AcceptsValidInput: a typical
t.TempDir()/abc.jsonl input still flows through to
store.SessionJobsDir, caches correctly, and a follow-up
StartForSession writes the artifact next to the transcript.
This PR is independent of #6936: validateSessionPath does not call
validatePathSegment, so the two can be reviewed and merged in either
order. Both fixes share the underlying issue #6932 threat model.
* fix(jobs): validate session paths across platforms
Problem: SetActiveSessionPath traversal validation missed Windows-style backslash components on Unix and changed the legacy empty-path behavior.
Root cause: filepath.ToSlash only rewrites the current platform separator, and validation ran before the active-only empty-path branch.
Fix: Normalize both separator styles explicitly, keep empty paths as active-only calls, document the trusted-path boundary, and add regression coverage.
Verification: go vet ./internal/jobs/...; go test -count=1 ./internal/jobs/...; go test -race -count=1 ./internal/jobs/...; go test -count=1 ./...
Co-authored-by: SivanCola <[email protected]>
* fix(jobs): preserve trusted session path semantics
Problem: Trusted transcript paths containing valid .. or host-specific separator characters were rejected, and rejection left active-session routing and persistent bindings stale.
Root cause: The validator conflated lexical path syntax with trusted-root containment and returned before updating independent session-routing state.
Fix: Limit trusted-path validation to control characters, preserve the caller path spelling, update active routing on rejection, clear stale bindings, and add regression coverage for relative paths and invalid rebinding.
Verification: go vet ./internal/jobs/...; go test -count=1 ./internal/jobs/...; go test -race -count=1 ./internal/jobs/...
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* feat(desktop): add regional pricing and English localization
Problem: Desktop cost estimates assumed a single CNY price table, while automatic session titles and several desktop options remained Chinese-only for English users.
Root cause: Currency selection was coupled to persisted language settings, browser locale never reached controller rebuilds, cost telemetry lacked consistent regional repricing, and fallback copy was not localized at the API boundary.
Fix: Add official CNY/USD regional pricing with auto locale detection and an explicit desktop override, preserve structured currency telemetry and custom prices, localize session titles and missing desktop options, and make generated titles follow the user's language.
Verification: Passed config/boot/serve and full desktop Go suites, frontend typecheck and production build, focused frontend behavior tests, the cache guard, and git diff checks.
* fix: preserve regional pricing and manual topic titles
Problem:
Auto regional pricing could overwrite persisted official price tables, and manually entered default-like topic titles were localized.
Root cause:
Pricing normalization did not retain TOML provenance, while title localization inferred automation from title text alone.
Fix:
Carry regional pricing provenance through config loading, normalization, backfill, and edit replay; only explicit currency choices override persisted tables. Propagate topic title source through desktop runtimes and localize only auto-generated default titles.
Verification:
go test ./internal/config ./internal/boot ./internal/serve -count=1
cd desktop && go test ./... -count=1
* chore: credit regional localization contributors
Context: PR #6945 consolidates regional pricing preservation and English session-title work from three related contributor PRs.
Attribution: Record the source authors with their public GitHub noreply identities so the consolidated repository history preserves their contributions.
Verification: Attribution-only commit; product files are unchanged.
Co-authored-by: dmitri-0 <[email protected]>
Co-authored-by: erphenheimer <[email protected]>
Co-authored-by: delight-f <[email protected]>
* feat(cli): add regional pricing controls
Context: PR #6945 already provides regional DeepSeek pricing in desktop, but CLI startup, in-session settings, and JSON result output did not expose the same currency contract.
Changes: Resolve Auto pricing from the active CLI locale, add global currency config and slash commands, refresh the current controller after language or currency changes, and emit currency-aware total_cost while retaining total_cost_usd as a compatibility alias. Reject mixed-currency totals and prevent project config from overriding user regional preferences.
Verification: go test ./internal/cli ./internal/config ./internal/boot ./internal/control ./internal/i18n -count=1; go test ./... -count=1 with DEEPSEEK_API_KEY unset.
* docs(cli): document regional pricing controls
Documentation:
- Add English and Chinese examples for global currency selection and locale-based Auto.
- Explain structured cost/currency output and the total_cost_usd compatibility alias.
- List the /currency session command and runtime refresh behavior.
Verification:
- go test ./internal/cli -run "Test(ConfigCurrency|RunOutput)" -count=1
- git diff --check
---------
Co-authored-by: dmitri-0 <[email protected]>
Co-authored-by: erphenheimer <[email protected]>
Co-authored-by: delight-f <[email protected]>
When a session is resumed or its history replayed, orphaned
[Pasted text #N] labels that were never expanded (because
pastedBlocks was lost during a prior reload) are stored raw in
the transcript. The model receives an opaque token it cannot
interpret.
repairOrphanedPasteLabels runs during loadSessionUnlocked:
1. Scans the same transcript for prior expansions (--- Begin/End ---)
2. Inlines recovered content into orphaned labels
3. Replaces unrecoverable labels with [pasted content unavailable]
This cleans historical pollution at load time, complementing the
TUI-layer fix (chat_tui_paste.go) that prevents new orphaned
labels on submit.
Ref: #6948
* feat(cli): add privacy-preserving telemetry and crash reports
Summary:
- add bounded CLI usage metrics with explicit policy controls and silent retry behavior
- capture sanitized panic reports locally and require user confirmation before upload
- separate Desktop and CLI aggregates in the crash-report worker and stats dashboard
- document telemetry fields, privacy guarantees, report commands, and D1 migration requirements
Compatibility:
- default missing client surfaces to Desktop for existing clients and payloads
- migrate existing aggregate rows to the Desktop surface before deploying the new worker
Verification:
- go test ./internal/telemetry ./internal/crashreport ./internal/config ./internal/cli ./cmd/reasonix
- env -u DEEPSEEK_API_KEY go test ./...
- pnpm test (workers/crash-report, 49 tests)
- pnpm run typecheck (workers/crash-report)
- ./scripts/cache-guard.sh
* fix(stats): decouple CLI schema migration from Worker deploy
Problem:
The initial surface migration rebuilt the existing Desktop telemetry tables and changed their primary keys. Running it before the new Worker broke the released Worker's ON CONFLICT clauses, while deploying the Worker first queried columns that did not exist yet.
Root cause:
Desktop and CLI isolation was implemented by changing a shared persisted schema, which made the old and new Worker contracts mutually exclusive during rollout.
Fix:
Keep the released Desktop tables and primary keys unchanged, route CLI telemetry to additive cli_* tables, and let the new Worker initialize those tables with idempotent CREATE IF NOT EXISTS statements. Keep the migration additive and identical to the runtime bootstrap so migration, deployment, and rollback order no longer affect Desktop ingestion.
Verification:
- pnpm test (workers/crash-report, 54 tests)
- pnpm run typecheck (workers/crash-report)
- git diff --check
* fix(test): make crash report mode check portable
Problem:
The Windows CI job failed because os.FileMode reports synthesized POSIX bits on Windows rather than the 0600 mode requested by AtomicWriteFile.
Root cause:
The test treated Unix permission bits as a cross-platform ACL signal even though Windows enforces file access through inherited ACLs.
Fix:
Keep the 0600 assertion on Unix-like systems and skip only that non-portable mode-bit comparison on Windows while retaining all report creation, sanitization, and bounded-retention checks.
Verification:
- go test ./internal/crashreport
- GOOS=windows GOARCH=amd64 go test -c ./internal/crashreport
* fix(cli): preserve actionable crash report groups
Problem:
CLI panic reports used runtime/debug.Stack as their top frame, so unrelated panics collapsed into one fingerprint. Queue pruning also deleted reports written in an unknown future format.
Root cause:
Top-frame extraction selected the first source location without filtering capture/runtime frames, and pruning treated every JSON file as a current report.
Fix:
Select the first application frame with its sanitized function name, retain a safe fallback, and prune only recognized current-schema report files. Reject newer schemas from old-client listing so their fields remain untouched.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- go test -race ./internal/crashreport ./cmd/reasonix
- go vet ./...
- pnpm test (workers/crash-report, 54 tests)
- pnpm run typecheck (workers/crash-report)
- Windows cross-compile of internal/crashreport
* fix(cli): request consent before telemetry upload
Problem:
A fresh release-build interactive CLI treated a missing telemetry setting as auto and could start anonymous uploads without first informing the user.
Root cause:
The runtime default and persisted decision state were conflated, and telemetry startup had no consent owner before the reporter launched its background flush.
Fix:
Preserve a missing cli_metrics field as an undecided state, show a localized one-time [Y/n] disclosure before the first eligible upload, persist auto or off before continuing, clean pending counters on rejection, and fail closed when saving fails. Keep CI, Safe Mode, development, environment opt-outs, and non-interactive sessions silent and disabled until a decision exists.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- go test -race ./internal/telemetry ./internal/cli
- go vet ./...
- pnpm test (workers/crash-report, 54 tests)
- pnpm run typecheck (workers/crash-report)
- ./scripts/cache-guard.sh
- git diff --check
* fix(jobs): reject path traversal in artifact parentSession and kind
Issue: #6932
StartForSession trimmed parentSession but did not reject path separators
or '..' components. Combined with filepath.Join(tempRoot, parentSession, id)
and the subsequent os.MkdirAll + os.OpenFile, a caller controlling the
session id or kind could write artifact files outside the manager's temp
root, subject to the running process's filesystem permissions.
This change:
* Validates parentSession and kind at StartForSession entry. A validation
failure short-circuits before any goroutine or artifact is created; the
job is registered as Failed with the validation error in artifactErr so
list / Wait calls observe it.
* Adds a defense-in-depth containment check in openArtifactLocked: the
resolved dir must remain inside m.tempRoot. This catches any future
path that reaches openArtifactLocked through a different route (e.g.
SetActiveSessionPath with a malicious sessionPath).
* Adds unit tests in jobs_security_test.go covering:
- validatePathSegment table (valid + every rejected character class)
- StartForSession with traversal parentSession (no file written)
- StartForSession with traversal kind (no file written)
- StartForSession with normal input (regression guard)
- openArtifactLocked with an out-of-root dir in artifactDirs
* docs(jobs): clarify artifact containment boundary
Problem: The validator and regression-test comments implied every accepted artifact path remains under the manager temp root.
Root cause: The explanation overlooked trusted persistent sidecar directories bound by SetActiveSessionPath, which intentionally live next to transcripts.
Fix: Clarify that path-segment validation confines only the temp-root fallback while persistent directories come from trusted store paths.
Verification: go test -count=1 ./internal/jobs/...; go test -count=1 ./...
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* fix(permission): harden dynamic Bash authorization
Problem:
Broad Bash allow rules could silently authorize nested command and process substitutions, while treating every dynamic shell form as human-required would make Auto unnecessarily disruptive.
Root cause:
Bash rule matching did not distinguish reusable static commands, exact-only dynamic syntax, and nested or indirect execution before applying scoped allows and controller bypasses.
Fix:
Classify Bash ASTs into reusable, exact-only, and human-required tiers. Restrict all dynamic approvals to exact Bash=<literal> grants, require a human for nested or indirect execution outside YOLO, preserve Auto fallback for expansions, assignments, redirects, heredocs, and globs, and apply the same contract across interactive, headless, persistence, CLI, and ACP paths.
Verification:
- go test ./internal/shellparse ./internal/permission ./internal/control ./internal/boot ./internal/cli ./internal/acp
- go test -race ./internal/control
- go vet ./...
- env -u DEEPSEEK_API_KEY go test ./...
- gofmt -l .
- git diff --check
Fixes#6963
* fix(control): remove unused approval wrapper
Problem:
The CI lint job rejected PR #6985 because requestApprovalDecision was left unused after approval paths moved to the option-aware helper.
Root cause:
The dynamic Bash approval refactor replaced the wrapper call sites but did not remove the obsolete wrapper itself.
Fix:
Remove the unused wrapper without changing approval behavior.
Verification:
- go run github.com/golangci/golangci-lint/v2/cmd/[email protected].2 run --timeout=5m
- go test ./internal/shellparse ./internal/permission ./internal/control ./internal/boot ./internal/cli ./internal/acp
- go test -race ./internal/control
- gofmt -l .
- git diff --check
* fix(permission): preserve dynamic compound rule precedence
Problem:
Dynamic compound Bash commands could reach fallback before per-segment Deny or Ask rules were evaluated. Raw rule matching also lost the established :* prefix semantics, and brace expansion or extglob arguments could be treated as reusable static commands.
Root cause:
The approval class short-circuited compound decomposition, the raw matcher interpreted Bash prefix markers as ordinary glob text, and the AST feature pass did not normalize brace expansion before testing whether words were static.
Fix:
Run compound segments through the same complete policy recursively, preserve Bash prefix semantics with AST-aware raw matching, prioritize wildcard tokens in glob matching, and classify brace/extglob or other non-static arguments as exact-only expansions using mvdan syntax nodes.
Verification:
- go run github.com/golangci/golangci-lint/v2/cmd/[email protected].2 run --timeout=5m
- go test ./internal/shellparse ./internal/permission ./internal/control ./internal/boot ./internal/cli ./internal/acp
- go test -race ./internal/control
- go vet ./...
- env -u DEEPSEEK_API_KEY go test ./...
- gofmt -l .
- git diff --check
* fix(permission): detect indirect builtin execution
Problem:
Bash builtin eval and builtin source could inherit broad Bash authorization even though their direct forms require human approval.
Root cause:
The indirect-execution classifier recursively handled command, exec, env, and other wrappers, but omitted Bash's builtin wrapper.
Fix:
Treat builtin as a recursive command wrapper and add positive and negative regression cases so eval/source require a human while ordinary builtins stay reusable.
Verification:
- go test ./internal/shellparse ./internal/permission ./internal/control ./internal/boot ./internal/cli ./internal/acp
- go test -race ./internal/control
- go vet ./...
- gofmt -l .
- git diff --check
* fix(config): prevent .mcp.json plugins from leaking into reasonix.toml / 防止 .mcp.json 插件泄漏到 reasonix.toml
RenderTOMLProjectDelta (used by saveProjectIncremental) now filters out
plugins sourced from .mcp.json (MCPSourceProjectMCPJSON). Previously,
saving the config after Load() merged .mcp.json servers into c.Plugins,
the TOML renderer would write them into reasonix.toml. On next load,
their Source provenance changed from project_mcp_json to project_config,
invalidating launch grants and activation receipts keyed by ConfigSource.
Fixes#6702.
* fix(config): preserve MCP plugin ownership on save
Problem:
Scoped TOML saves could copy merged MCP entries into the wrong config file and could leave a removed project plugin behind while foreign-source entries remained in memory.
Root cause:
The project delta filtered only .mcp.json entries, while full rendering and plugin-section removal still used the unfiltered runtime plugin list.
Fix:
Centralize destination-aware plugin filtering for full and incremental renders, retain unknown provenance for directly constructed configs, and mark explicit upgrade imports as user-owned before saving.
Verification:
- go test ./internal/config
- go vet ./internal/config
- ./scripts/cache-guard.sh
- git diff --check
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: jiangrenhang <[email protected]>
Co-authored-by: SivanCola <[email protected]>
* fix(machine): resolve project session scope for --dir
* test(machine): pin project session directory mapping
* fix(machine): preserve explicit session directory scope
Problem:
The PR reinterpreted the documented --dir session-store argument as a project root, which made existing machine Session, Task, and Recovery callers return empty or not-found results.
Root cause:
A single flag was used for two distinct path scopes instead of preserving the existing contract and adding an explicit project selector.
Fix:
Keep --dir as the session storage directory, add --project-root for project-scoped lookup, share directory resolution across Session and Task commands, reject conflicting selectors, and document both modes in English and Chinese.
Verification:
- go test ./internal/cli -count=1
- go test -race ./internal/cli with focused machine tests
- go vet ./internal/cli
- env -u DEEPSEEK_API_KEY go test ./... -count=1
- ./scripts/cache-guard.sh
- git diff --check
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* fix: honor configured Bash for Windows plugin hooks
Problem:
Windows plugin hooks that explicitly request Bash can fail after upgrade even when a usable non-standard Git Bash path is configured. Expanded plugin paths can also remain in a form that Git Bash cannot consume.
Root cause:
Hook execution and diagnostics rediscovered Bash independently instead of receiving the effective configured shell path, while Windows plugin-root expansion did not normalize shell-form commands for Bash.
Fix:
Propagate the configured Bash runtime through hook execution and diagnostics, normalize expanded Windows plugin paths for explicit Bash hooks, and report missing runtime dependencies in CLI and Desktop doctor surfaces. Add Superpowers and custom-path regression coverage plus bilingual documentation updates.
Verification:
go test ./internal/hook ./internal/capdiag ./internal/boot ./internal/cli
go test -race ./internal/hook ./internal/capdiag
env -u DEEPSEEK_API_KEY go test ./...
(cd desktop && go test ./...)
go vet ./internal/hook ./internal/capdiag ./internal/boot ./internal/cli
(cd desktop && go vet ./...)
Windows amd64 cross-compilation for affected root and Desktop packages
git diff --check
Cache-impact: none
* fix: remove obsolete Windows hook wrappers
Problem:
The CI unused-code linter rejects two compatibility wrappers that became unreachable after Hook execution started injecting the effective Bash runtime.
Root cause:
The runtime-aware call sites moved to resolver-injected helpers, but the old zero-argument resolver wrappers remained in the Windows compatibility module.
Fix:
Remove the two obsolete wrappers and keep the shared runtime-aware parsing helpers and cached Bash discovery path.
Verification:
go test ./internal/hook ./internal/capdiag ./internal/boot ./internal/cli
go vet ./internal/hook ./internal/capdiag ./internal/boot ./internal/cli
gofmt -l internal/hook/windows_compat.go
git diff --check
bash scripts/check-cache-impact.sh with the PR metadata
Cache-impact: none
* test: normalize Superpowers Bash hook expectation
Problem:
The Windows CI test expected a host-native backslash plugin root even though explicit Bash hooks intentionally receive a forward-slash path.
Root cause:
The Superpowers 6.2.0 regression assertion joined the temporary plugin root without applying the same platform-independent slash contract that the Hook loader guarantees for Bash.
Fix:
Normalize the expected plugin root with filepath.ToSlash so the test verifies the intended Git Bash command on Windows and remains stable on Unix hosts.
Verification:
go test ./internal/hook
Windows amd64 test-binary compilation for internal/hook
gofmt -l internal/hook/hook_test.go
git diff --check
Cache-impact: none
* docs(release): prepare v1.18.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): refine v1.18.0 notes
Summary:
Clarify Kimi K3 support across the official Kimi APIs and OpenCode Go, retain the distinct reasoning scales, and record every affected product surface. Keep the Desktop and native CLI channel copy aligned with the merged release contract.
Verification:
Validate the catalog, run the release-notes test suite, render both languages, and check the diff for whitespace errors.
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Add native Kimi K3 support for OpenCode Go
Problem:
Reasonix did not expose Kimi K3 in the OpenCode Go subscription preset even though the upstream catalog now serves it.
Root cause:
The curated OpenCode Go model catalog and capability metadata predated Kimi K3, while the generic OpenAI-compatible provider clamped max reasoning effort to high.
Fix:
Add Kimi K3 with vision, 1M context, and max-only reasoning metadata; preserve explicitly supported max effort; safely migrate untouched legacy presets; and synchronize desktop previews and documentation.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- cd desktop && go test ./...
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend exec tsx src/__tests__/provider-model-refresh.test.ts
- ./scripts/cache-guard.sh
* Fix Kimi K3 compatibility paths
Problem:
Kimi K3 max-only effort metadata was omitted by reasonix-guard assist, and legacy normalization replaced existing custom Kimi K3 overrides.
Root cause:
The recovery command duplicated provider configuration assembly, while the migration assigned the Kimi K3 override unconditionally.
Fix:
Forward resolved effort and capability metadata through the recovery path, merge only missing Kimi K3 defaults, preserve case-insensitive custom override keys, and add regression coverage.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- cd desktop && go test ./...
- ./scripts/cache-guard.sh
- git diff --check
* Support Kimi K3 high and max effort
Problem: Kimi K3 was advertised as max-only even though the service accepts both high and max, so /effort high was rejected and capability displays were incomplete.
Root cause: The preset, legacy catalog migration, desktop mock, documentation, and regression fixtures all encoded the old max-only capability.
Fix: Advertise high and max while keeping max as the default, and update every compatibility path and focused regression test. Existing custom Kimi K3 overrides remain unchanged.
Verification: env -u DEEPSEEK_API_KEY go test ./...; (cd desktop && env -u DEEPSEEK_API_KEY go test ./...); pnpm --dir desktop/frontend typecheck; pnpm --dir desktop/frontend exec tsx src/__tests__/provider-model-refresh.test.ts; ./scripts/cache-guard.sh; git diff --check.
* Adapt official Kimi APIs for K3
Problem: The PR exposed Kimi K3 only through OpenCode Go, leaving the official Kimi CN and Global presets without the model or its documented request contract.
Root cause: The direct-API catalogs predated K3, while the generic OpenAI-compatible wire shape drops assistant reasoning, forwards configurable temperature, and uses max_tokens.
Fix: Add K3 capability overrides and conservative legacy catalog migration for both official endpoints; preserve complete assistant messages; omit fixed sampling fields; map output limits to max_completion_tokens; and keep relay behavior isolated.
Verification: env -u DEEPSEEK_API_KEY go test ./...; cd desktop && env -u DEEPSEEK_API_KEY go test ./...; pnpm --dir desktop/frontend typecheck; pnpm --dir desktop/frontend exec tsx src/__tests__/provider-model-refresh.test.ts; ./scripts/cache-guard.sh; git diff --check.
* Preserve K3 reasoning through remote broker
Problem:
Remote Workbench lost the Kimi K3 reasoning round-trip policy when the Desktop provider was represented by the Host broker proxy. A configured PostLLM hook could therefore replace the reasoning persisted for the next official K3 request.
Root cause:
The new ReasoningRoundTripPolicy was implemented by the OpenAI-compatible client and consumed by the Agent, but the broker descriptor and hostProvider only forwarded the older tool-call reasoning capabilities.
Fix:
Carry an optional reasoningRoundTrip capability through provider descriptors and the Remote wire schema, expose it from hostProvider, and add an end-to-end broker regression that proves display-transformed reasoning still replays the original provider text.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- go test -race ./internal/remote/broker
- go vet ./...
- go test ./... (desktop module)
- pnpm typecheck (desktop/frontend)
- go run ./cmd/remote-protocol-gen --check
- ./scripts/cache-guard.sh
* Preserve custom vision settings during K3 migration
Problem: Kimi K3 migration appended vision support to every matching provider, including configurations that explicitly disabled or customized vision models.
Root cause: provider-level vision_models distinguishes an omitted nil list from an explicit empty or user-curated list, but the migration ignored that distinction.
Fix: add Kimi K3 vision support only for omitted lists or exact legacy preset lists, and preserve explicit empty and custom lists for direct Kimi and OpenCode Go migrations. Add regression coverage for both paths.
Verification: go test ./...; ./scripts/cache-guard.sh; go run ./cmd/remote-protocol-gen --check; git diff --check
Refresh the bilingual v1.18.0 catalog against the latest main-v2 release boundary, including #6155 channel updates and #6944 signing-gate hardening.
All required CI, CodeQL, release-notes, cache, and platform checks passed.
When a user opens a folder, createEmptySessionFile writes the new
session's BranchMeta sidecar with an empty Scope field. During the
tab controller build, if resolveSessionBinding reaches
sessionBindingFromMeta, the empty Scope causes DefaultScope() to
return global, and applySessionBindingToTab overwrites the tab's
correct project scope. The wrong value is persisted to
desktop-tabs.json while desktop-projects.json remains correct.
The fix adds pinSessionBranchMeta, called immediately after
createEmptySessionFile, which writes the correct scope, workspace
root, and topic ID into the BranchMeta before the controller build
reads it — closing the empty-Scope window regardless of which
binding path is taken.
Closes#6914.
When a session is reloaded (recovery chain, model switch, resume),
the in-memory pastedBlocks slice is cleared. If the user later
re-submits an input that still references a [Pasted text #N] label
(via history navigation or quoted reply), expandPastedBlocks could
not find the matching block and the raw placeholder was sent to the
model — the model saw an unexpanded token instead of the pasted
content.
Recover orphaned labels by scanning conversation history for prior
submissions that contain the expanded --- Begin/End --- block and
re-expanding from there. Labels with no recoverable content are
stripped so the model never receives a raw placeholder.
Ref: #6948
advance() previously wrote interceptMsg directly to the goalMachine
struct field as a side effect. Multiple FSM mechanisms (todo intercept,
self-check, idle detection) could each set interceptMsg, relying on
sequential ordering within the switch statement to avoid contention.
Extract intercept as an explicit field in goalAdvanceResult. The
caller (advanceGoalAfterTurn) stores it via a new setIntercept()
method, which takeIntercept() reads as before. This makes the data
flow explicit: advance() produces output, the caller routes it.
No behavior change. One theoretical edge case (auto-turn limit firing
simultaneously with an intercept) has a different interceptMsg state
after advance() returns, but the loop exits via cont=false immediately
so the intercept is never consumed and gets cleared on the next goal
lifecycle event.
Related to #6958 (FSM output model refinement).
The concrete bug #6956 was fixed separately in PR #6960.
When the agent signals [goal:blocked], the goal FSM increments the
blocks counter but did not reset g.idleTurns. If blocks < 3 (so
notice remains empty) and the agent makes no tool calls for 2
subsequent turns, the idle detection at maxGoalIdleTurns fires and
sets an interceptMsg that overrides the agent's blocked signal.
Add g.idleTurns = 0 in the GoalStatusBlocked case so that an explicit
blocked signal resets the idle counter. The agent is not idling — it
deliberately signalled a blocking condition.
Fixes#6956
When rapidly switching between saved sessions on the same tab, the old
session's runtime is detached (controller kept alive in detachedSessions)
if it was actively running. Switching back triggers a rebuild path that
tries to acquire an OS-level file lock already held by the detached
controller's file descriptor — LockFileEx with LOCKFILE_EXCLUSIVE_LOCK
on Windows prevents a second handle from the same process.
Fix: before entering the build path, check if the target session has a
detached runtime. If found, reattach it via attachExistingSessionRuntime
instead of building a new controller. This avoids the lease conflict
entirely and is more efficient.
Also fix canReclaimCurrentProcessSessionLease to check detachedSessions,
preventing incorrect reclaim attempts against detached runtimes.
Fixes#6955
When baseState.revision == currentRevision the revision ledger proves
no external writer has touched the transcript. In that case the
byte-level messagesHavePrefix comparison in checkSnapshotWrite is
redundant — the revision CAS already vouches for consistency — and its
false positives (normalisation / JSON round-trip / local-only metadata)
drive spurious diverged conflicts that cascade into recovery-branch
chains and force-saves that break the prefix-cache.
Future-tense (Delta): anchor on the revision CAS match, not the
byte-level disk snapshot (Gamma). Same insight as Agda PR #8611
(makeTau's nTarget = nOld + nctel - 1).
Ref: #6027 Phase 3, #6948
Problem: Stable release v1.17.21 reached SignPath through release-stable.yml while the provider policy allowed only release-desktop.yml, blocking both Windows architectures after CLI and npm publication.
Root cause: The provider-side build-definition allowlist was maintained manually, and the stable orchestrator did not prove the complete signing path before starting public publishers.
Fix: Add a machine-readable signing contract, workflow call-graph validation, protected fingerprints, exact CODEOWNERS coverage, a zero-publication dual-architecture preflight, and fail-closed standalone attestation checks.
Verification: Full PR CI, CodeQL, Windows Desktop packaging, race, coverage, release workflow contract tests, cache guard, root and Desktop Go tests, vet, actionlint, npm dry build, and diff checks passed.
MiniMax uses Anthropic-style field names (input_tokens / output_tokens /
cache_read_input_tokens / cache_creation_input_tokens) on its
OpenAI-compatible endpoint instead of the standard OpenAI/DeepSeek
names. normaliseUsage() only recognised the latter, so MiniMax cache
stats were always zeroed out, causing the frontend to display
'cache not reported'.
Fix: add InputTokens, OutputTokens, CacheCreationInputTokens and
CacheReadInputTokens to wireUsage as fallbacks. normaliseUsage()
now resolves token counts and cache stats through a priority chain:
DeepSeek top-level -> OpenAI nested details -> Anthropic-style fields.
This benefits all OpenAI-compatible providers without provider-specific
branching.
Test: TestNormaliseUsageMiniMax covers cache-hit, cache-creation,
and DeepSeek field priority scenarios.
Context:
Reasonix needed one public Stable/Preview contract across native CLI archives and Desktop updates while keeping npm prerelease tags independent.
Changes:
- Add protected native CLI Preview prereleases without moving Homebrew or GitHub Latest.
- Add persistent Desktop Stable/Preview selection, channel-aware update state, R2 pointers, and legacy Canary compatibility.
- Preserve the complete production Windows payload and installer signing chain and fail closed when signing or Preview R2 prerequisites are unavailable.
- Add release gateway routing, workflow contracts, compatibility coverage, and bilingual settings copy.
Compatibility:
Missing and unknown Desktop channel settings remain Stable. Legacy canary, beta, and next settings normalize to Preview. New Preview clients reject stale test-signed Canary manifests while older clients retain the mirrored canary pointer.
Verification:
Root and Desktop Go suites, frontend test:all and production build, crash-report Worker typecheck and 45 tests, release workflow contracts, cache guard, release notes tests, six native CLI target builds, CodeQL, race, and Windows NSIS/portable CI all pass on head 97623c982.
* docs(release): prepare v1.17.21 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): complete v1.17.21 notes
Summary:
Expand the release surfaces beyond Desktop and add the missing CLI diagnostics and recovery ownership changes from PR #6912.
Verification:
Validated the release catalog, rendered the bilingual notes, ran the catalog tests, and checked the diff for whitespace errors.
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(desktop): sync typography code-region when global mono font changes (#6907)
When the user switches the global monospace font in Appearance settings,
reset any previously-customised TypographySettings code-region override
(followGlobal=true) so the global choice takes effect in code blocks
instead of being silently blocked by the --typography-code-font inline
style set by applyTypographyPreferences.
Also replace three hardcoded font-family: monospace occurrences
(.remote-file-view__path, .remote-file-view__editor,
.remote-hostkey-dialog__fp) with var(--font-mono, monospace), and
align .code-lines-wrap with the other code-block rules by using
var(--typography-code-font, var(--font-mono)).
* fix(desktop): preserve regional code font overrides
Problem: Selecting a global monospace font silently forced an explicit Code & Tools typography override back to follow-global, while valid quoted preset font stacks were rewritten unnecessarily.
Root cause: The settings handler treated the regional override as stale, and the original patch assumed quoted multi-word family names were invalid after CSS custom-property substitution.
Fix: Keep regional typography independent when the global monospace choice changes and restore quoted multi-word font family names.
Verification: pnpm --dir desktop/frontend exec tsx src/__tests__/typography-preferences.test.ts; pnpm --dir desktop/frontend typecheck; git diff --check.
Co-authored-by: SivanCola <[email protected]>
* test(desktop): protect regional code font behavior
Problem: The code-font fix relied on CSS inheritance and settings ownership contracts that were not covered by focused regression tests.
Risk: A future global monospace handler could silently overwrite an explicit regional choice, or nested code elements could fall back to the browser monospace default again.
Coverage: Render Appearance with a customized code region and verify a global monospace change preserves its stored preference and CSS variable. Assert the nested code and line-number viewer font declarations directly.
Verification: pnpm --dir desktop/frontend exec tsx src/__tests__/settings-refresh-snapshot.test.tsx; pnpm --dir desktop/frontend exec tsx src/__tests__/typography-overflow-contract.test.ts; git diff --check.
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* feat(desktop): add bounded regex search to code previews
Problem:
Workspace code previews only exposed literal search, the find shortcut was scoped to the focused viewer, and the floating search popup could obscure code.
Root cause:
JavaScript regular expressions run synchronously without cancellation, the workspace panel had no search action of its own, and the popup used absolute positioning inside the code viewport.
Fix:
Run opt-in regex searches in an inline worker with hard termination, debounce, stale-request cancellation, and pattern, source, and result limits. Add a visible workspace search action and panel-level shortcut, dock the search toolbar above code, preserve literal search by default, and localize bounded failure states.
Verification:
- pnpm --dir desktop/frontend test
- pnpm --dir desktop/frontend build
- pnpm --dir desktop/frontend exec tsx src/__tests__/line-number-code.test.tsx
- pnpm --dir desktop/frontend exec tsx src/__tests__/workspace-changes-errors.test.tsx
- pnpm --dir desktop/frontend exec tsx src/__tests__/workspace-preview-css.test.ts
- git diff --check
- Browser verification at the narrow dock width
* Fix regex result handling and search toolbar copy
Problem: Valid regex patterns such as .* lost non-empty matches when they also emitted a zero-length result, and the floating copy button remained hidden under the open toolbar.
Root cause: Match collection rejected on the first zero-length result, while the copy affordance was rendered independently of search state.
Fix: Skip zero-length matches with Unicode-safe advancement, report unsupported only for zero-only patterns, move copy into the toolbar while search is open, and let narrow toolbar actions wrap as a group.
Verification: pnpm test; pnpm build; git diff --check; in-app browser verification at a 242 px code-pane width.
* fix(desktop): preserve pending code search requests
* fix(desktop): bound and clarify regex preview search
Problem: Regex worker startup could remain pending without entering the 200 ms timeout, and line-by-line evaluation silently changed regular-expression semantics.
Root cause: The deadline was created only after the async worker import resolved, while regex matching reset state for every source line.
Fix: Start the deadline before worker creation, scan regex requests against the full source with line anchors preserved, and surface explicit localized errors for newline-spanning matches. Add regression coverage for both paths.
Verification: node --import tsx src/__tests__/line-number-code.test.tsx; node --import tsx src/__tests__/workspace-changes-errors.test.tsx; node --import tsx src/__tests__/workspace-preview-css.test.ts; pnpm typecheck; pnpm test:typecheck; pnpm build; git diff --check.
* fix(tui): add paste undo on Windows via Ctrl+Z (#6531)
On Windows Ctrl+Z has no job-control semantics, so repurpose it as
undo for paste operations. A LIFO undo stack (capped at 50 entries)
records the input state before each paste; Ctrl+Z pops and restores.
Folded pastes and image refs participate without double-pushing.
* fix(desktop): implement paste undo via explicit undo stack (#6531)
Replace the unreliable browser-native undo approach (execCommand /
setRangeText in WebView2) with an explicit paste undo stack in the
Composer. Before each paste the current input text is pushed onto a
ref-managed stack; a Ctrl+Z keydown handler pops and restores the
last saved state. Falls through to browser-native undo when the
stack is empty, preserving normal text-editing undo.
* Expose composer undo and redo controls
Problem:
Composer undo and redo behavior was not discoverable in Settings, help, context menus, or the user guides. YOLO tooltips also kept showing its default binding after a rebind.
Root cause:
The composer handled editing chords and Reasonix transactions locally instead of registering the actions in the shared shortcut catalog. The custom context menu exposed clipboard actions only, and documentation still described Ctrl/Cmd+Y as YOLO-only.
Fix:
Register locked platform undo and redo actions, protect their editing chords from legacy global bindings, reuse the transaction history from the context menu, render live YOLO shortcut labels, and update English and Chinese guidance.
Verification:
pnpm test:all
pnpm build
go test ./... in desktop
Real browser checks for Settings rows, live YOLO rebind labels, context-menu undo/redo, and a clean console
* Fix rich composer edit context menu
Problem:
The rich composer did not open Reasonix's edit context menu, so drafts containing skill or subagent tokens could not use the transaction-aware Undo and Redo actions.
Root cause:
Only the plain textarea wired the shared context-menu handler; RichComposerInput did not expose or forward a contextmenu event.
Fix:
Forward context-menu events from RichComposerInput and reuse the existing Composer edit menu for both editable surfaces. Add regression coverage for undoing and redoing a paste after an inline invocation.
Verification:
- pnpm exec tsx src/__tests__/composer-goal-toggle.test.tsx
- pnpm exec tsx src/__tests__/composer-session-draft.test.tsx
- pnpm exec tsx src/__tests__/composer-context-menu-clipboard.test.tsx
- pnpm test:typecheck
- pnpm build
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* Protect composer history from legacy YOLO bindings
Problem:
Legacy custom YOLO bindings on Ctrl/Cmd+Z or Ctrl/Cmd+Shift+Z intercepted
Composer undo and redo, toggling approval mode instead of editing the draft.
Root cause:
Composer matched the local YOLO shortcut before its transaction-aware history
handler and did not apply the reserved editing-chord policy used globally.
Fix:
Exclude reserved Composer undo and redo chords from local YOLO handling. Add
upgrade regression coverage for legacy bindings, rebound YOLO, and the default
Ctrl+Y behavior.
Verification:
- pnpm exec tsx src/__tests__/composer-session-draft.test.tsx (130 passed)
- pnpm exec tsx src/__tests__/keyboard-shortcuts.test.ts (71 passed)
- pnpm exec tsx src/__tests__/composer-goal-toggle.test.tsx (185 passed)
- pnpm exec tsx src/__tests__/shortcut-recorder.test.tsx (25 passed)
- pnpm test:typecheck
- pnpm exec vite build --logLevel error
- git diff --check
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* fix: redact sensitive data from error logs
Problem: CodeQL reported two error logging paths that could expose credentials embedded in runtime errors.
Root cause: The heartbeat and bot controller failure paths logged raw error values, while the stronger authentication scrub was private to the control package.
Fix: Add shared credential and error redaction helpers, mask URL userinfo, reuse the helper for authentication errors, and sanitize both reported logging sinks.
Verification: go test ./...; cd desktop && go test ./...; git diff --check.
* fix: close redaction edge cases
Problem: URL userinfo containing raw @ characters could leave a password suffix visible, and repeated credential redaction changed already-redacted KEY=value output.
Root cause: The userinfo regexp stopped at the first @, while redactKeyValues remasked the stronger helper's **** marker as [redacted].
Fix: Consume the complete userinfo segment through the final @ and preserve existing redaction markers during key/value masking. Add regressions for raw @ proxy credentials and idempotent KEY=value errors.
Verification: go test ./...; git diff --check.
Problem:
The repository had twelve open Dependabot alerts across the site, desktop frontend, and Cloudflare Worker development toolchains.
Root cause:
Direct dependency ranges and generated lockfiles still resolved vulnerable releases of Astro, PostCSS, sharp/libvips, SVGO, DOMPurify, and js-yaml.
Fix:
Upgrade the site to Astro 7.1.3, refresh the affected frontend transitive dependencies, and move all Worker projects to Wrangler 4.114.0 with compatible workers-types 5.x releases.
Verification:
- Site tests and Astro static production build
- Desktop TypeScript check, Mermaid sanitizer tests, and Vite production build
- Worker TypeScript checks and Wrangler deployment dry-runs
- Crash-report Worker test suite
* feat: add deterministic planner routing and depth
Problem:
The two-model coordinator invoked the planner for nearly every work-shaped turn, adding latency to questions, contextual follow-ups, and clearly atomic edits while producing plans with inconsistent engineering detail.
Root cause:
The planner gate returned only a boolean and inferred intent from the composed prompt, so it could not represent planning depth, host-owned approval, trusted turn state, or observable routing reasons.
Fix:
Introduce deterministic executor-only, plan-and-execute, and plan-for-approval routes with light/full depth and privacy-safe reason codes. Feed the gate pristine user text plus trusted Goal, Delivery, Plan Mode, synthetic, and conversation metadata. Add stable planner contracts, bounded per-turn research, host-enforced plan approval, executor validation guidance, documentation, and regression matrices.
Verification:
- ./scripts/cache-guard.sh
- go test ./internal/control ./internal/agent ./internal/boot -count=1
- go vet ./internal/control ./internal/agent ./internal/boot
- env -u DEEPSEEK_API_KEY go test ./... -count=1
- focused race tests for planner routing, metadata, approval, and research budgets
Cache impact:
Low. The planner system prompt changes once to define stable light/full contracts; per-turn route and depth remain in the user turn. Tool schemas and ordering are unchanged, and the cache guard remains above its 90 percent threshold.
* fix: preserve planner handoff semantics
Problem:
Bare plan-first requests were classified as approval-only turns, so they could stop after planning instead of reaching the executor.
Root cause:
The deterministic directive table conflated plan-and-execute, plan-for-approval, and explicit no-execution intent.
Fix:
Add a distinct plan-only route, reserve plan-for-approval for explicit confirmation language, route bare plan-first requests through the executor, and preserve both execution boundaries on planner failure. Update English and Chinese contracts plus regression coverage.
Verification:
- go test ./internal/agent ./internal/control
- env -u DEEPSEEK_API_KEY go test ./...
- go vet ./...
- ./scripts/cache-guard.sh
* fix(planner): preserve execution after bounded planning
Problem: Task-first plan-only and approval boundaries could be missed, while a planner research-budget pause stranded ordinary work without a usable executor continuation.
Root cause: Routing only recognized leading planning directives, and Coordinator preserved tool-loop pauses even though planner research limits are host-owned per-turn policy.
Fix: Parse unquoted execution boundaries across the request with negation handling; roll back incomplete planner turns; fall back to the executor with the original task for ordinary plan-and-execute while keeping plan-only and approval routes fail-closed. Update docs and regression coverage.
Verification: go test ./... -count=1; go test -race ./internal/control ./internal/agent -count=1; go vet ./...; ./scripts/cache-guard.sh; live Planner-to-Executor tests with DeepSeek, LongCat, and GLM Coding Plan.
Co-authored-by: SivanCola <[email protected]>
* test(planner): verify plan-only continuation
Problem: Plan-only coverage proved that execution was suppressed and the plan was persisted, but did not prove that a later user command actually reached the executor with that saved plan.
Fix: Add a two-turn coordinator regression that plans without execution, then sends an execution continuation and verifies the executor receives the persisted plan while the planner is not invoked again.
Verification: go test ./internal/agent -count=1
Co-authored-by: SivanCola <[email protected]>
* fix(planner): skip bounded read-only actions
Problem: Short read-only commands such as running tests, reviewing a PR, or inspecting one file still invoked the planner and could be upgraded to a full plan in Delivery mode.
Root cause: Read-only verbs shared the generic work-term bucket with mutations, and anchored work was evaluated before ambiguous scope.
Fix: Detect requests containing only read-only work terms and route bounded non-ambiguous cases directly to the executor. Preserve full planning for high-risk, cross-surface, structured, complex, or ambiguous read-only work, and prioritize ambiguous scope over an anchored target. Update English/Chinese docs and the routing matrix.
Verification: go test ./... -count=1; go test -race ./internal/control ./internal/agent -count=1; go vet ./...; ./scripts/cache-guard.sh.
Co-authored-by: SivanCola <[email protected]>
* fix(release): sign Windows payload executables
Problem:
Windows Defender can quarantine reasonix-desktop.exe after install or update even though the downloaded NSIS installer is signed.
Root cause:
The release workflow applied Authenticode only to the finished NSIS container. The desktop executable, support binaries, portable archive entries, and generated uninstaller remained unsigned after extraction.
Fix:
Sign the exact Windows payload through a versioned SignPath configuration, rebuild the portable archive and NSIS installer from those signed files, verify the payload before signing the outer installer, and fail official releases closed when signing is unavailable. Preserve and inject a signed NSIS uninstaller in the second packaging pass.
Verification:
- go test ./... (desktop module)
- env -u DEEPSEEK_API_KEY go test ./...
- bash scripts/release-workflows.test.sh
- ./scripts/cache-guard.sh
- actionlint release workflows
- bash syntax and git diff checks
* fix(release): separate canary installer signing config
* fix(release): automate gated SignPath approvals
Problem:
Release certificates require one SignPath approval per request, causing four extra manual approval actions after the GitHub release gate.
Root cause:
The desktop workflow submitted each request synchronously and waited for an interactive SignPath approver even though the same release authority had already approved the protected GitHub environment.
Fix:
Submit requests asynchronously, validate the expected signing policy, approve them through the dedicated CI identity, poll to a final successful state, and securely download the signed artifacts. Exercise the same path in canary with an approval-enabled test policy and document the administrator contract.
Verification:
- cd desktop && go test ./...
- bash scripts/release-workflows.test.sh
- actionlint release-stable.yml release-desktop.yml
- git diff --check
* fix(release): default SignPath API endpoint
The GitHub connector does not expose a SignPath API URL when requests are submitted asynchronously. Use SignPath's documented SaaS endpoint by default and keep the script override available for self-hosted installations.
Verified with desktop Go tests, release workflow contract tests, actionlint, and git diff --check.
* fix(release): restrict production signing origin
Problem:
Repository-level SignPath credentials were available to workflows on ordinary repository branches, while the production signing policy allowed every branch origin. A writer could therefore construct an unreviewed workflow that requested a trusted production signature.
Root cause:
Stable and prerelease builds ran directly from tag origins, so the SignPath policy could not use one exact protected branch without breaking tag releases. The policy had consequently been configured with an unrestricted branch wildcard.
Fix:
Relay stable and desktop prerelease tags to the current workflow on protected main-v2, keep immutable tags as build candidates, add a gated non-publishing production-signing smoke mode, and codify the exact main-v2 SignPath origin in tests and administrator documentation.
Verification:
- bash scripts/release-workflows.test.sh
- actionlint v1.7.12 on all changed workflows
- cd desktop && go test ./...
- cd desktop && go test . -run signing contract tests
- ./scripts/cache-guard.sh
- git diff --check
* fix(release): wait for manual smoke approval
Problem:
The first production-certificate smoke run failed immediately after submitting its two payload requests when the CI identity had intentionally been removed from the production approver list.
Root cause:
The request completion helper always called the SignPath Approve API as soon as a request entered WaitingForApproval, even in the explicitly human-reviewed smoke mode.
Fix:
Add an external-approval mode that polls without approving, enable it only for production signing smoke runs, and document and test the manual verification boundary. Normal canary and release automation retains CI approval.
Verification:
- bash scripts/release-workflows.test.sh
- actionlint v1.7.12 on release-desktop.yml
- cd desktop && go test . -run signing contract tests
- git diff --check
* fix(cli): isolate TUI diagnostics and recovery ownership
Problem:
Background structured logs and plugin stderr could overwrite the Bubble Tea composer. Recovery branches could also continue without the CLI session lease, and post-autosave metadata rewrites could be misclassified as same-revision conflicts.
Root cause:
The interactive CLI left the process logger and initial boot stderr attached to the terminal, did not wire OnSessionRecovered to its lease keeper, and classified some in-place session mutations separately from the mutation itself.
Fix:
Route interactive diagnostics through a private bounded log, propagate the diagnostic writer and recovery callback through every controller build, move the lease before recovery commits, and make persisted-history rewrites atomic.
Verification:
- go test -race ./internal/cli ./internal/control ./internal/agent ./internal/guardian
- env -u DEEPSEEK_API_KEY go test ./...
- go build ./cmd/reasonix
- Windows amd64 test-binary cross-compile
* fix(cli): preserve recovery lease across all runtimes
Problem:
The recovery lease callback was only wired into interactive chat. Headless run and serve sessions could move to a recovery branch while still guarding the original path, and serve controller rebuilds could publish a post-snapshot recovery path before its lease followed.
Root cause:
Session recovery ownership lived in a CLI-specific helper, serve attached its keeper only to explicit HTTP binding operations, and model switches rebound the pre-snapshot path after publishing the replacement controller.
Fix:
Move the recovery handoff onto SessionLeaseKeeper, install it for chat, run, serve, and rebuilt serve controllers, and bind the replacement controller's actual post-snapshot path before publishing it. Preserve the outgoing controller on binding failure and keep lease errors path-free.
Verification:
- go test -count=1 ./internal/control ./internal/cli ./internal/serve
- go test -race -count=1 ./internal/control ./internal/cli ./internal/serve
- env -u DEEPSEEK_API_KEY go test -count=1 ./...
- cd desktop && go test -count=1 ./...
- go vet ./...
- go build ./cmd/reasonix
* Fix plugin hook execution contracts
Problem: Windows plugin hooks with compound commands or nested quoting could still fail after the narrow quoted-batch compatibility fix, while explicit empty args could lose exec-form semantics across JSON round trips.
Root cause: Hook launch behavior was inferred from argument length and platform shell heuristics instead of preserving the manifest's exec-form or shell-form contract. CMD and PowerShell command strings also passed through incompatible quoting layers.
Fix: Preserve args field presence and shell selection through plugin parsing and loading, add explicit legacy/exec/shell execution modes, transport raw CMD and encoded PowerShell scripts without token re-rendering, retain the existing simple batch compatibility path, and document the cross-platform contract.
Verification: Focused Hook and plugin package tests, race and shuffled tests, fuzzing, root and Desktop full suites, go vet, and Windows/Linux cross-compilation.
* Fix Windows hook output regressions
Problem: The new compound CMD regression expected cmd.exe to strip quotes from %1, leaving the Windows job red. PowerShell hook scripts also preserved UTF-16LE input transport without forcing captured output to UTF-8.
Root cause: The test confused %1 with the dequoting %~1 form, and the Hook-specific EncodedCommand path did not reuse the shell runtime's existing PowerShell output-encoding prologue.
Fix: Align the CMD assertion with native batch semantics, share the PowerShell UTF-8 script wrapper across shell and Hook launchers, and exercise Chinese plus emoji output in the Windows runtime test.
Verification: Focused Hook/plugin/sandbox tests and vet; race/shuffle count 3; root and Desktop full suites; full vet; Windows amd64 test cross-compilation; PowerShell encoding fuzzing.
* feat(desktop): add native crash diagnostics
Problem:
Windows users could report that the desktop closed and would not reopen, but native process exits, UI-thread stalls, WebView2 failures, and window-restore hangs left too little durable evidence for analysis.
Root cause:
The desktop only persisted panics from explicitly wrapped goroutines in one overwrite-prone file. Startup tracking stopped distinguishing a healthy process that later vanished, and Windows native/WebView2/window-restore failure paths were not connected to the diagnostic pipeline.
Fix:
Capture Go runtime fatal output, retain a bounded backward-compatible report queue, heartbeat startup lifecycle state, and attribute abnormal exits to coarse uptime, install, and update buckets. Add a Windows Wails-window watchdog, WebView2 process-failure classification, and durable window-restore journaling. Extend aggregate metric ingestion and the health dashboard, while redacting panic values, secrets, and personal path segments and documenting the telemetry behavior.
Verification:
- cd desktop && go test ./...
- cd desktop && go test -race . -run focused crash queue tests
- go test -race ./internal/repair -run TestStartupTracker
- Windows amd64 cross-compile of the desktop test binary
- desktop frontend typecheck, focused settings tests, and production build
- crash-report worker: 42 tests and TypeScript typecheck
- git diff --check
* test(desktop): make hang assertions platform-aware
Problem: Windows desktop CI expected macOS crash labels and metric buckets even though the watchdog correctly emits Windows-specific diagnostics.\n\nFix: derive the expected label, error type, and metric bucket from the same platform identity helpers used by the implementation.\n\nVerification: targeted macOS tests pass; Windows amd64 test binary cross-compiles successfully.
* fix(desktop): remove crash watchdog stalls
Problem: the Windows UI watchdog allocated a permanent syscall callback on every one-second heartbeat, and the window-restore journal used a crash-safe fsync/retry path immediately before showing the window.
Root cause: Windows callbacks cannot be released and the Go runtime has a finite callback table; AtomicWriteFile can also wait on antivirus-held rename locks.
Fix: register one serialized EnumWindows callback for the process, keep the restore journal best-effort without fsync/retry, and add long-run Windows plus journal regression coverage.
Verification: go test ./... (desktop); targeted race tests; go vet ./...; Windows amd64 cross-compile; cache guard.
* fix(desktop): make crash diagnostics process-safe
Problem:
Window restore diagnostics recorded success after Wails only queued UI-thread work, and routine duplicate launches shared the primary process's fatal-output file.
Root cause:
The restore timeout watched the WindowShow call instead of native window state, while fatal output used one process-global path before Wails enforced its single-instance lock.
Fix:
Wait asynchronously for the Windows window to become visible and non-minimised, fence superseded restore attempts, and retain the journal until a real acknowledgement. Store fatal output and structured-panic markers per PID, process only dead owners, and keep backward-compatible legacy dump ingestion.
Verification:
- cd desktop && go test ./...
- cd desktop && go test -race .
- cd desktop && go vet ./...
- Windows amd64 desktop test cross-compile
- ./scripts/cache-guard.sh
- git diff --check
* fix(desktop): canonicalize runtime process identity
Problem: executable-based single-instance IDs let stable, canary, and portable builds concurrently write the same Reasonix home, while path aliases could split session lease identity.
Root cause: process ownership was keyed by binary location and fresh paths resolved only their immediate parent.
Fix: hash the canonical REASONIX_HOME and resolve every missing path component through the nearest existing symlink or junction ancestor, preserving isolated homes and dev-mode parallelism.
Verification: Go agent tests, Desktop tests, Windows cross-compilation, and canonical-path race tests pass.
* fix(desktop): unify writable session runtime ownership
Problem: Goal and Delivery mode rebuilds, duplicate tab restore, background reattachment, and saved-session navigation could build a second controller for one session or tear down the live source before a target was proven usable.
Root cause: controller startup, lease ownership, deferred retries, detached-session attachment, and path switching were coordinated per WorkspaceTab without a canonical process-wide admission record or one atomic controller/lease/path commit.
Fix: add a SessionRuntime registry keyed by runtime ID and canonical session key; attach or wait on existing local runtimes; safely reclaim only orphaned current-process leases; and make saved-session switches build, restore, validate, and lease a candidate before atomically publishing it. Target failures preserve the source controller, lease, path, mode profile, and epoch.
Verification: full Desktop and repository tests, Desktop/Agent/Controller/lease race suites, all 18 collaboration-token-mode orderings, deterministic A/B/A navigation fencing, frontend tests/build, Wails bindings, and a production Desktop build pass.
Co-authored-by: erphenheimer <[email protected]>
* fix(frontend): fence sends and events by runtime state
Problem: the composer could append an optimistic user bubble while a workspace was starting or lease-blocked, and late events from a replaced controller could mutate the new transcript.
Root cause: frontend readiness used only legacy booleans and controller events had no process-runtime generation fence.
Fix: expose structured runtime phase and issue metadata, gate every submit path before optimistic dispatch, seed and advance per-tab runtime epochs, and ignore events from obsolete epochs while retaining legacy event compatibility.
Verification: the full frontend suite, focused send-failure tests, TypeScript checking, and the production Vite build pass.
* docs: record desktop session ownership fix
Problem: the user-visible impact and compatibility boundary of the Desktop self-lock fix were not documented.
Fix: describe single-runtime session ownership, epoch-fenced events, readiness-gated sends, and REASONIX_HOME-scoped single-instance behavior in the Unreleased changelog.
Verification: Markdown diff inspection and repository privacy scan pass.
* test(desktop): align CI fixtures with runtime ownership
Problem: CI found an unused private runtime helper and a Linux timing path where a Heartbeat test injected Ctrl plus Ready without publishing the matching SessionRuntime state.
Fix: remove the unused helper and advance the injected test runtime exactly as the production controller publication path does; production readiness checks remain strict.
Verification: the Heartbeat submission and pending-prompt tests pass for 20 repetitions and for 5 race-enabled repetitions, and go vet passes.
* fix(desktop): wait for starting session runtimes
Problem: two persisted tabs restoring the same session could attach the first tab's starting placeholder before its controller was published, retire both candidate builds, and leave the visible session permanently starting.
Root cause: startup called the general attach path before claimSessionRuntime classified the registered phase, and applyRuntimeTab could create a second registry record when adopting a legacy runtime into a target that already owned a placeholder.
Fix: make claimSessionRuntime the startup admission entry, refuse attachments from non-ready runtime owners, reuse the target placeholder when adopting compatible visible or detached runtimes, and cover the starting-to-ready duplicate-tab handoff.
Verification: focused runtime tests passed for 10 repetitions; the runtime/mode/navigation race matrix passed for 3 repetitions; Desktop go test ./..., go test -race ., go vet ./..., go build ./..., and git diff --check passed.
---------
Co-authored-by: erphenheimer <[email protected]>
* fix(mcp): trust and surface project servers
Problem:
Project-provided MCP servers were blocked behind a separate launch authorization flow and were not clearly distinguished from global servers in Desktop or CLI management views.
Root cause:
Project scope was coupled to launch approval, while configured-server lookup and presentation did not preserve project provenance consistently.
Fix:
Treat project MCP configuration as user-authorized by default, preserve workspace-scoped activation and relative working directories, load project config for reconnects, and group project, global, and managed servers consistently across Desktop and CLI.
Verification:
- ./scripts/cache-guard.sh
- go test ./...
- cd desktop && go test ./...
- pnpm build
- pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
* fix(mcp): preserve config scopes and precedence
Problem:
Global MCP installs and project declarations shared persistence paths, so Desktop and CLI actions could write the wrong config, store activation against the wrong workspace, or remove same-name declarations across scopes.
Root cause:
Runtime config merging preserved the effective entry but persistence call sites did not consistently carry its owning source or the controller workspace.
Fix:
Write explicit installs to the user-global config, keep project edits in reasonix.toml or .mcp.json, apply deterministic project-over-global precedence, and restore the next same-name fallback after removing an override. Align Desktop, CLI, TUI, imports, Planner-facing runtime state, and documentation with the same rules.
Verification:
- go test ./...
- go vet ./...
- cd desktop && go test ./...
- cd desktop && go vet ./...
- cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- cd desktop/frontend && pnpm run typecheck
- cd desktop/frontend && pnpm build
- ./scripts/cache-guard.sh
- git diff --check
* test(mcp): satisfy staticcheck in handler
Problem:
The PR lint job rejected the new MCP test handler with staticcheck QF1003.
Root cause:
The handler used an if/else-if chain on the same request method value.
Fix:
Use a tagged switch for initialize and tools/list responses without changing test behavior.
Verification:
- go test ./internal/control
- git diff --check
* feat: trust configured project hooks by default
Problem:
Project hooks required a separate workspace trust action even though configured MCP servers and installed plugin hooks already use low-friction trusted defaults. This added a second confirmation model and left Desktop, CLI, diagnostics, and documentation describing a trust state users should not need to manage.
Root cause:
Hook loading and inspection were gated by a persistent trust.json store, with that state propagated through boot, slash commands, Wails settings, diagnostics, and compatibility payloads.
Fix:
Load project hooks automatically whenever a project root is present, while preserving Safe Mode as the emergency disable path. Remove the trust store and user-facing trust controls, and keep legacy CLI commands, Wails methods, LoadOptions.Trusted, and trusted_project JSON as backward-compatible no-ops or enabled values. Update tests, diagnostics, localization, and documentation to describe automatic loading.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- cd desktop && go test ./...
- pnpm typecheck
- pnpm check:css
- pnpm exec tsx src/__tests__/diagnostics-settings.test.tsx
- ./scripts/cache-guard.sh
- Desktop browser validation for project Hooks
- git diff --check
* fix: keep MCP auth cleanup in the active workspace
Problem: The TUI clear-auth action followed the process working directory instead of the active controller workspace, and Desktop CI was blocked by an orphaned helper.
Root cause: Authentication cleanup bypassed the workspace selection used by the MCP manager, while the removed project-trust flow left an unused Desktop path helper behind.
Fix: Centralize TUI MCP workspace selection, add a cross-workspace regression test, remove the unused helper, and document the executable repository-config boundary with Safe Mode recovery guidance in English and Chinese.
Verification: go test ./internal/cli; go vet ./...; go test ./...; cd desktop and run go vet ./..., go build ./..., and go test ./...; ./scripts/cache-guard.sh; git diff --check.
* fix(mcp): preserve advertised cold-cache connect calls
Problem:
A provider could call a cold-cache MCP connect placeholder that had been advertised at request serialization time, but background schema discovery could replace it before dispatch and produce an unknown-tool failure.
Root cause:
Tool calls are resolved against the current registry after the provider returns, while cache-miss discovery intentionally swaps the synthetic connect placeholder for the server's real tools.
Fix:
Recognize a stale synthetic connect call as completed when the same MCP namespace now exposes real tools, and let already-resolved lazy placeholders report the same controlled result. Keep genuine unknown calls unchanged and add regressions at both dispatch and lazy-lifecycle boundaries.
Verification:
- go test ./...
- go test -race ./internal/plugin ./internal/agent
- go vet ./internal/plugin ./internal/agent
- ./scripts/cache-guard.sh
- delayed cold-cache LongCat MCP integration test
DeepSeek API rejects image_url content parts with HTTP 400
"unknown variant 'image_url', expected 'text'". When the DeepSeek
protocol is detected, convert image attachments to text references
("[Attached image: filename.png]") instead of image_url parts.
This prevents the 400 error while still informing the model that
images were attached. Tool-result images are also skipped for
DeepSeek since they would produce the same error.
Fixes#6682.
* fix(desktop): keep provider retries cancellable
Problem: During provider retry backoff, a stale idle tab snapshot could clear the frontend running state, hiding Stop until all retry attempts finished.
Root cause: The retrying event only updated retry counters even though it is authoritative evidence that the foreground turn remains active and cancellation-aware.
Fix: Restore running, turn-active, cancellable, and timing state when handling retrying events, with a regression that reproduces the stale snapshot ordering.
Verification: Focused controller progress, cancel reconciliation, and composer tests pass; frontend typecheck, CSS checks, and git diff checks pass.
* fix(desktop): fence retry state from stale snapshots
Record retry activity on the same monotonic clock used by runtime snapshot fetches. Ignore idle snapshots started before the latest retry so Stop and Escape cancellation stay available, while allowing a fresh idle snapshot to reconcile a missed turn_done and clear the retry indicator.
* fix(recovery): reduce Auto failure interruptions
Problem:
Auto injected a long guard message after ordinary tool and verification failures, then treated unrelated follow-up work as recovery that could reach the reviewer or a user confirmation. Recovery pause copy could also appear duplicated or in the wrong language.
Root cause:
The recovery gate treated an active failure as a task-wide boundary, did not distinguish transient timeouts, and allowed non-plan reviewer outcomes to escalate to human prompts. Localized clients also rendered the backend English fallback alongside localized copy.
Fix:
Classify failure evidence, isolate retries to the exact failed operation, keep unrelated and host-proven read-only work automatic, and reserve human decisions for structured plan changes. Add bounded timeout guidance, backward-compatible persistence, localized recovery_paused copy, and matching desktop, CLI, serve, and documentation coverage.
Verification:
- go test ./... -count=1 with DeepSeek credentials unset
- go test -race ./internal/recovery ./internal/control -count=1
- go vet ./internal/recovery ./internal/agent ./internal/control
- ./scripts/cache-guard.sh
- go test ./... -count=1 in desktop/
- pnpm typecheck and focused frontend recovery tests
* fix(recovery): enforce runtime retry boundaries
Problem:
Runtime-shaped recovery proposals always carried a preview, so their fingerprint did not match completed observations and the exact-operation failure limit was not enforced. Tool-owned MCP deadlines were also reported as cancellations and skipped transient recovery classification.
Root cause:
One fingerprint served both operation accounting and preview-bound approvals, despite those records having different shapes. The Agent adapter also inferred cancellation from the returned error cause instead of the parent turn context.
Fix:
Separate preview-free operation identity from preview-bound approval identity, and treat a deadline as cancellation only when the parent context ended. Add regressions for real proposal previews, internal MCP deadlines, and parent deadline preservation.
Verification:
- go test ./... -count=1 with DeepSeek credentials unset
- go test -race ./internal/recovery ./internal/agent ./internal/control -count=1
- go vet ./internal/recovery ./internal/agent ./internal/control ./internal/cli ./internal/serve
- ./scripts/cache-guard.sh
- go test ./... -count=1 in desktop/
- focused frontend recovery tests and pnpm typecheck
* docs(release): prepare v1.17.20 notes
Summary:
Add reviewed bilingual stable release notes for v1.17.20, covering the machine CLI interfaces, trusted MCP delegation, Debian updater bootstrap, recovery changes, and desktop reliability fixes.
Compatibility:
Document the one-time Debian package bootstrap, matching Desktop/Remote host requirement, machine identity initialization, and installed-MCP trust boundary without changing runtime behavior.
Verification:
- node scripts/release-notes.mjs validate
- rendered the v1.17.20 GitHub release notes successfully
- node --test scripts/release-notes.test.mjs
- bash scripts/release-workflows.test.sh
- git diff --check
* ci(release): reuse release-bound pull requests
Problem:
Preparing reviewed bilingual notes opened a dedicated pull request for every stable version even when an existing final product or release-infrastructure pull request already defined the candidate boundary.
Root cause:
The preparation workflow always selected a version-specific release-notes branch and had no validated way to update an existing review branch.
Fix:
Add an optional target_pr path that accepts only an open same-repository pull request into main-v2 containing the latest trunk. Reuse that branch, expose the rendered draft in the workflow summary, retain the dedicated pull request as a fail-closed fallback, and document the release-PR frequency rule.
Verification:
- bash scripts/release-workflows.test.sh
- node scripts/release-notes.mjs validate
- node --test scripts/release-notes.test.mjs
- rendered the v1.17.20 release notes successfully
- git diff --check
* feat(cli): add redacted machine interfaces
* fix(cli): repair machine lifecycle projections
Problem:
Persisted running jobs could be resurrected after restart with a closed done channel and no cancel function, while task projections reported running subagents as finished and artifact-complete. Machine session IDs also exposed model-bearing transcript names.
Root cause:
Writing start metadata invalidated the loader's terminal-only assumption, and the machine projection treated update timestamps and file allocation as terminal completion.
Fix:
Normalize restored running jobs to an interrupted terminal state, keep in-flight artifacts incomplete, project stale state from the session lease, verify subagent artifact presence, hash machine session identities, reject conflicting event-output flags, and document the schema contract in English and Chinese.
Verification:
- go test ./internal/jobs ./internal/cli
- go test -race ./internal/jobs
- go test -race ./internal/cli -run machine-interface focused tests
- go vet ./internal/jobs ./internal/cli
- ./scripts/cache-guard.sh
- root test sweep passed all deterministic packages; the invalid-key live probe was isolated and its package passed with DEEPSEEK_API_KEY unset
* fix(jobs): preserve live tasks across runtime rebuilds
Problem:
A replacement jobs manager could rewrite a still-running background task as interrupted while the original controller continued executing it. CLI and serve runtime switches could trigger that overlap because they only guarded foreground turns.
Root cause:
Persisted running artifacts did not identify their live manager, so every loader assumed they belonged to a dead runtime. Runtime rebuild entry points also used inconsistent active-work checks.
Fix:
Add a private optional manager owner ID to job metadata, defer artifacts owned by a live in-process manager, and reload them after terminal completion. Normalize model, effort, skill, work-mode, and serve rebuild guards to reject foreground work, pending prompts, or background jobs.
Verification:
- go test ./...
- go test -race ./internal/jobs
- go test -race ./internal/cli ./internal/serve
- go vet ./internal/jobs ./internal/cli ./internal/serve ./internal/i18n
- ./scripts/cache-guard.sh
* fix(jobs): require session ownership for stale repair
Problem:
A manager loading persisted running job artifacts could rewrite them as interrupted while another Reasonix runtime still owned and wrote the same transcript. Bot and Remote Workbench sessions also did not retain transcript leases across their full lifecycle.
Root cause:
Process-local manager owner IDs cannot prove that a missing owner is dead in another process, and the artifact loader had no session-write ownership check. Bot and Remote create, restore, fork, rotate, and close paths did not consistently acquire, transfer, and release session leases.
Fix:
Inject a fail-closed session ownership probe into jobs managers, repair abandoned running artifacts only while the current process holds the transcript lease, and keep leases attached to Bot and Remote sessions until controller teardown. Add regression coverage for denied repair, lease transfer, concurrent restore, and close-time release.
Verification:
- go test ./internal/jobs ./internal/agent ./internal/control ./internal/bot ./internal/remote/workbench/runtime ./internal/boot
- go test -race ./internal/jobs ./internal/agent ./internal/bot ./internal/remote/workbench/runtime
- go vet ./internal/jobs ./internal/agent ./internal/control ./internal/bot ./internal/remote/workbench/runtime ./internal/boot
- go test ./... (all deterministic packages passed; the invalid local DeepSeek credential probe passed with DEEPSEEK_API_KEY unset)
- cd desktop && go test ./...
- ./scripts/cache-guard.sh
* test(remote): release session leases before cleanup
Problem:
Windows CI could not remove Remote runtime TempDir trees because two tests ended while the new session lease lock files were still open.
Root cause:
The tests created or rotated leased runtime sessions but never closed their Server instances before testing.TempDir cleanup. Unix permits unlinking open files, while Windows correctly rejected the cleanup.
Fix:
Register snapshotAndClose cleanup for the rotation fixture and broker integration test so controllers close and session leases release before temporary directories are removed.
Verification:
- go test ./internal/remote/workbench/runtime -run 'TestSessionRotationRegistryFailureReturnsCommittedEpoch|TestRuntimeControllerUsesDesktopBrokerWithoutHostKey' -count=1
- GOOS=windows GOARCH=amd64 go test -c -o /tmp/pr6859-runtime.test.exe ./internal/remote/workbench/runtime
- git diff --check
* fix(cli): preserve legacy run session IDs
Problem:
Existing json and stream-json automation received a hashed session_id, so the documented resume query could no longer resolve the value emitted by a run.
Root cause:
runAgent applied the new redacted machine-session transform before finalizing every structured output format instead of limiting it to events-jsonl.
Fix:
Select the final session ID by output format: preserve the established raw branch ID for text, json, and stream-json, and hash only events-jsonl. Add focused compatibility coverage for every format.
Verification:
- go test ./internal/cli -run 'TestRunOutput(SessionIDPreservesExistingFormats|JSONResult|EventsJSONLIsStructuredAndRedacted)' -count=1
- go test ./internal/cli ./internal/jobs ./internal/agent ./internal/bot ./internal/remote/workbench/runtime ./internal/serve ./internal/boot
- go test -race ./internal/cli ./internal/jobs
- merged with origin/main-v2: go test ./internal/cli ./internal/jobs
- merged with origin/main-v2: go test -race ./internal/cli
- merged with origin/main-v2: ./scripts/cache-guard.sh
- git diff --check
* fix(machine): fail closed on lifecycle repair errors
Problem:
- Session rotation could leave remote and bot controllers serving an unleased transcript after lease transfer failed.
- Machine event JSONL exposed provider-controlled tool identifiers and names.
- Failed interrupted-job metadata repair could publish an in-memory state that was not durable.
Root cause:
- Post-mutation lease errors and recovery write errors were treated as recoverable without restoring a consistent ownership or persistence boundary.
Fix:
- Retire rotated remote and bot sessions when the replacement lease cannot be acquired.
- Replace provider tool identities with stable per-run opaque aliases.
- Defer interrupted tombstones when metadata repair fails, emit a warning, and retry on the next bind.
- Add focused regression coverage for each failure mode.
Verification:
- go test ./internal/cli ./internal/jobs ./internal/bot ./internal/remote/workbench/runtime
- go test -race ./internal/cli ./internal/jobs ./internal/bot ./internal/remote/workbench/runtime
- go test ./internal/agent ./internal/control ./internal/serve ./internal/boot
- env -u DEEPSEEK_API_KEY go test ./...
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check
* fix(machine): harden IDs and preserve active runtimes
Problem:
- Runtime profile and bot routing switches could close controllers that still owned pending prompts or background jobs.
- Machine session IDs used an unkeyed hash of guessable transcript branch names, allowing offline enumeration.
Root cause:
- Lifecycle guards only covered foreground turn state and did not serialize bot admission with runtime replacement.
- The public ID derivation had no installation-secret entropy.
Fix:
- Gate Remote close/profile operations and Bot routing rebuilds on running turns, pending prompts, and background jobs, with a second pre-swap check and an admission-serialized Bot switch.
- Initialize a private 32-byte per-install key under the Reasonix state directory with a cross-process lock and atomic 0600 write, derive 128-bit HMAC session IDs, and fail closed on corrupt identity state.
- Load the key before events-jsonl provider execution, document first-use initialization, and add focused lifecycle, concurrency, and privacy regressions.
Verification:
- go test ./internal/cli ./internal/remote/workbench/runtime ./internal/bot -count=1
- go test -race ./internal/remote/workbench/runtime ./internal/bot ./internal/cli ./internal/jobs -count=1
- go vet ./...
- env -u DEEPSEEK_API_KEY go test ./... -count=1
- cd desktop && go test ./... -count=1
- ./scripts/cache-guard.sh
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* fix(machine): bind lifecycle repair to live ownership
Problem:
Startup subagent cleanup could interrupt work owned by another process, repair authorization survived lease handoff windows, and hook machine output labeled unusable entries active.
Root cause:
Subagent cleanup rewrote a shared directory without parent lease ownership, one registry mixed pending and active lease generations, and hook status did not reuse actual loader and matcher semantics.
Fix:
Acquire each parent transcript lease before stale subagent repair, publish only active lease generations and revoke them before unlock, and classify hook executability with shared event matcher semantics. Add deterministic cross-process, controlled handoff, invalid-hook, and compatibility regressions.
Verification:
- env -u DEEPSEEK_API_KEY go test ./... -count=1
- env -u DEEPSEEK_API_KEY go test -race ./internal/agent ./internal/jobs ./internal/bot ./internal/remote/workbench/runtime ./internal/cli ./internal/serve -count=1
- cd desktop && env -u DEEPSEEK_API_KEY go test ./... -count=1
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* fix(machine): verify projected runtime state
Problem:
- task JSON could report missing or failed artifacts as complete
- plugin context paths that only existed could appear executable
- session help described opaque machine IDs as branch IDs
Root cause:
- persisted metadata and filesystem readiness were projected separately
- hook status and diagnostics used existence-only checks
- localized usage text retained an earlier identifier name
Fix:
- fail closed unless a terminal job has a readable regular persisted artifact without an artifact error
- share plugin context usability checks across machine status and diagnostics
- correct machine session ID help in all supported locales and add regressions
Verification:
- go test ./...
- go test -race ./internal/agent ./internal/jobs ./internal/bot ./internal/remote/workbench/runtime ./internal/cli ./internal/serve
- go test ./... (desktop)
- go vet ./...
- ./scripts/cache-guard.sh
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* feat(agent): trust installed MCP in Planner and task sub-agents via stable use_capability
Problem:
The dual-model Planner and task/fleet children could not reliably query installed
MCP servers (e.g. databases) without readOnlyHint or dynamic mcp__ schemas, so
plans and delegated research were often ungrounded. Issue #6854.
Root cause:
Planner and strict/ordinary child registries either excluded MCP or required
readOnlyHint, and MCP tools entered provider-visible schemas so connect churn
broke prompt cache. Delivery alone had use_capability.
Fix:
- NewPlannerAgent with PlannerMCPExecution: authorized non-destructive MCP via
use_capability without readOnlyHint; destructive left for Executor.
- Session-shared MCPCapabilityRuntime (Host/specs/connections) with per-agent
use_capability frontends (isolated ledger/audit).
- Ordinary task/fleet children get full proxy (list/inspect/call); destructive
follows normal writer permissions.
- Strict read-only children share proxy schema/reuse but keep authorized +
readOnlyHint + non-destructive execution.
- Strip direct mcp__* from Planner and sub-agent registries; convert profile
MCP allowlists to capability IDs; action=list for discovery.
- NonDestructiveMCPExecutionIntent re-check before tools/call; Desktop MCP
tool card + docs.
Verification:
- go test ./internal/agent ./internal/boot ./internal/control ./internal/plugin ./internal/capability
- ./scripts/cache-guard.sh
- pnpm --dir desktop/frontend typecheck
- git diff --check
Fixes#6854
* fix(agent): close trusted MCP auth, connect gate, and review proxy gaps
Problem:
PR #6865 left three P1 boundaries incomplete: deferred MCP targets treated
!RequireLaunchApproval as authorized (bypassing Spec.Authorized), authorized
mcp_connect__* still entered the ordinary permission Gate, and review/read_only_skill
children never received the shared capability runtime or use_capability allowlist.
Root cause:
Connect helpers invented trust instead of using ServerAuthorized(); lifecycle
connect targets were excluded from the installed-MCP fast path; boot skill
runners still called registry builders without capRuntime and builtin review
AllowedTools omitted use_capability.
Fix:
- Deferred MCP targets and ensureServerTools use only Spec.ServerAuthorized().
- Authorized MCP tools and lifecycle connects share the explicit-deny-only gate.
- Wire capRuntime into read_only_skill and subagent skill runners; add
use_capability to builtin review/security-review allowlists.
- Regression tests for zero-start unauthorized non-project MCP and trusted
first connect under deny-all ordinary gates.
Verification:
- go test ./internal/agent ./internal/skill ./internal/plugin ./internal/boot
* fix(agent): stabilize restricted proxy prefix and isolate planner ledger
Problem:
Restricted use_capability proxies embedded dynamic capability IDs in their
Description, so MCP install/tool-list changes altered provider-visible tool
bytes (cache churn). Delivery dual-model still shared Executor use_capability
and ledger with the Planner. Restricted action=list leaked every configured
server. An empty if in subagent_registry_test triggered staticcheck SA4006.
Root cause:
Allowlist text was baked into tool descriptions; PlannerToolRegistry copied
parent use_capability before the clone branch; attachSubagentCapabilityProxy
returned early when a parent proxy was already present; list delegated to the
unfiltered frontend.
Fix:
- Keep restricted proxy description/schema identical to the base proxy; enforce
allowlists only in host checks and filtered list results.
- Always strip and reinstall an isolated use_capability frontend for sub-agents
and the Planner; give the Planner its own ledger and audit in boot.
- Filter list output to allowlisted servers only.
- Assert FilterReadOnlyRegistry keeps authorized RO MCP; add stability and
isolation regression tests.
Verification:
- go test ./internal/agent ./internal/boot ./internal/skill
- git diff --check
* fix(agent): fail-closed restricted MCP list filtering
Problem:
Malformed allowed-tools entries such as "mcp-server:" produced a non-empty
capability allowlist but an empty server set, so filterCapabilityListResult
returned the unfiltered inventory. Unreadable list payloads also passed through.
Root cause:
Incomplete capability IDs were accepted literally, and empty-server / parse
errors used fail-open returns.
Fix:
- Accept only complete mcp-server:<name> and mcp-tool:<server>/<tool> IDs.
- Skip installing a restricted proxy when no servers resolve from the allowlist.
- Empty server sets and JSON parse/encode failures return an empty servers list.
- Regression tests for malformed IDs, empty servers, and bad list payloads.
Verification:
- go test ./internal/agent -run 'TestRestricted|TestMalformed|TestFilterCapability|TestPlannerTool|TestCustomProfile'
* fix(ui): show resolved MCP capability effects
Problem:
The stable use_capability proxy made Desktop and CLI present MCP calls only by the proxy surface. Writer MCP calls could be grouped as quiet research, and terminal users saw the implementation-level proxy name instead of the selected capability.
Root cause:
Reasonix resolved the real MCP target only inside execution. That target name, capability ID, and read-only classification were not carried through durable session history, frontend events, or Remote history projections.
Fix:
- Persist optional resolved target metadata while keeping the provider-visible tool call unchanged.
- Emit deterministic same-ID dispatch refreshes after MCP resolution and carry the same classification on tool results.
- Classify Desktop MCP cards by the resolved effect, fail closed for legacy history, and preserve the metadata across Local and Remote reloads.
- Render use_capability as MCP(capability_id) in the CLI TUI and headless output.
- Add concurrency, serialization, history, protocol, Desktop, and CLI regression coverage.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- cd desktop && go test ./...
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend test
- go test -race ./internal/agent -run 'TestResolvedCapabilityDispatchRefreshesWriterClassification|TestResolvedCapabilityRefreshesParallelCallsInProviderOrder|TestUpdateToolCallResolutionPersistsAfterMidTurnSnapshot'
- ./scripts/cache-guard.sh
- go run ./cmd/remote-protocol-gen -check -root .
- git diff --check
* fix(agent): close trusted MCP proxy safety gaps
Problem:
Restricted sub-agent MCP allowlists could reveal sibling tool schemas, proxy-resolved writers could execute concurrently, and Balanced Executors could not directly call capability IDs discovered by their Planner.
Root cause:
Tool grants were widened to server IDs, batching trusted the proxy's read-only surface before resolving the real target, and dual-model Balanced installed use_capability only on the Planner registry.
Fix:
Keep tool grants exact and filter concrete tool inspection, serialize use_capability for every agent, and install isolated stable proxy frontends on both Balanced Planner and Executor while sharing the MCP runtime. Update bilingual contracts and add focused regressions.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- go test -race ./internal/agent
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check
* fix: enforce trusted MCP runtime boundaries
Problem:
Path-scoped sub-agents could resolve use_capability into unscoped MCP writers, and the stable MCP proxy kept boot-time specs after add, update, disable, or removal.
Root cause:
use_capability was classified as statically read-only before dynamic resolution, while MCPCapabilityRuntime and capability routing copied immutable boot snapshots that were not synchronized with controller lifecycle changes.
Fix:
Keep the provider-visible proxy contract stable while restricting explicit write_paths tasks to proven read-only MCP targets. Make each controller runtime an authoritative, race-safe MCP inventory, synchronize CLI/Desktop lifecycle changes and shared-host tabs, clear stale live tools, and route from live runtime snapshots.
Verification:
go test ./internal/agent ./internal/boot ./internal/control ./internal/plugin
go test -race ./internal/agent ./internal/control
env -u DEEPSEEK_API_KEY go test ./...
cd desktop && go test ./...
go vet ./...
./scripts/cache-guard.sh
git diff --check
* fix(agent): bind trusted MCP dispatch to live runtime identity
Problem:
Planner and stable capability frontends could reuse a same-name MCP client from another controller or stale configuration. Plan mode also blocked authorized opaque non-destructive MCP, lifecycle snapshots could mix generations, and legacy Desktop history lost built-in reader classification.
Root cause:
Shared Host reuse checked only the server name, resolved targets were not bound to the controller's current enabled/authorized spec, lifecycle mutations were not linearized with tools/call, and routing read configuration and live tools in separate snapshots. Documentation also described ordinary sub-agent MCP as using normal writer permission prompts even though installation is the trust boundary.
Fix:
- Compare complete host-local MCP runtime identity and require exact identity for Host/tool reuse.
- Bind resolved MCP targets to the live runtime and re-check enablement, authorization, identity, and destructive safety before dispatch.
- Linearize connect/call against update, disable, and removal; return atomic catalog/live-tool snapshots.
- Preserve Planner Plan-mode access to authorized non-destructive MCP without readOnlyHint and safely complete connected server-directory calls.
- Restore legacy Desktop reader classification and align bilingual contracts with explicit-deny-only installed MCP trust and direct-Executor cache limits.
- Add identity, shared-Host, real opaque Planner, revocation, concurrency, and frontend regressions.
Verification:
- go test ./internal/agent ./internal/plugin ./internal/control ./internal/boot -count=1
- go test ./internal/agent -run 'TestPlannerPlanModeExecutesAuthorizedOpaqueMCPThroughRuntime|TestRuntimeDisableLinearizesWithInFlightMCPDispatch|TestMCPCapabilityRuntimeConcurrentUpdatesAndSnapshots' -count=1
- go test ./internal/plugin -run TestMCPRuntimeSpecMatchesExactHostIdentity -count=1
- ./scripts/cache-guard.sh
- pnpm --dir desktop/frontend exec tsx src/__tests__/tool-subject.test.ts
- pnpm --dir desktop/frontend typecheck
- git diff --check
* fix(agent): remove obsolete MCP runtime wrapper
Problem:
The latest PR head failed golangci-lint because ensureServerTools remained as an unused private wrapper after every dispatch path moved to the identity-bound ensureServerToolsForSpec helper.
Root cause:
The runtime-identity refactor replaced all callers but left the compatibility wrapper behind. Go compilation and vet allow unused methods, while CI's unused analyzer correctly rejected it.
Fix:
Delete the dead wrapper so all connect paths continue to require the already-authorized current runtime spec explicitly.
Verification:
- go test ./internal/agent -count=1
- go vet ./...
- go run github.com/golangci/golangci-lint/v2/cmd/[email protected].2 run --timeout=5m
- git diff --check
* fix(agent): preserve MCP images and route IDs
Problem:
The first use_capability call to an unconnected MCP server dropped structured image results, and Planner transient routing rewrote concrete MCP capability IDs to source targets that use_capability cannot call.
Root cause:
onDemandMCPTool exposed only the text Execute interface, so Agent dispatch could not select ExecuteWithImages. RenderTransientBlock applied legacy source rewriting before accounting for Planner MCP proxy routing.
Fix:
Give the deferred MCP target a shared image-aware execution path that retains the runtime authorization lock through tools/call. Keep concrete MCP IDs for capability-proxy routes while preserving ordinary source routing for non-MCP capabilities. Add full Agent-path image coverage and focused route rendering regressions.
Verification:
- go test ./internal/agent ./internal/capability ./internal/control ./internal/boot
- env -u DEEPSEEK_API_KEY go test ./...
- go test -race ./internal/agent -count=1
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check
* fix(desktop): stabilize rich composer caret after invocation tags
Problem:
After selecting a skill or plugin invocation, mid-text edits in the desktop
rich composer often jumped the caret to the end of the draft on the first
keystroke, especially on Windows WebView2.
Root cause:
DOM→model counted user text that landed inside the NBSP caret anchor, but
model→DOM selection restore skipped the entire anchor. When selection was
temporarily unavailable, the code also faked the caret at text.length.
Fix:
Unify DOM walk rules for model reads, selection reads, and selection restore
so invocation chips stay zero-length while anchor body text remains editable.
Restore both selection ends, keep afterInvocationId behavior, and recover the
insertion point from a beforeinput snapshot when the browser drops selection.
Add focused rich-composer selection regressions and an Unreleased note.
Verification:
- pnpm --dir desktop/frontend exec tsx src/__tests__/rich-composer-selection.test.tsx
- pnpm --dir desktop/frontend exec tsx src/__tests__/composer-goal-toggle.test.tsx
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend build
* fix(desktop): recover rich caret after IME blackout and data-null repeats
Problem:
Two caret-recovery edges remained after the initial #6868 fix. IME
compositionend could restore the pre-composition offset when WebView2 dropped
selection, and data-null inserts of a repeated character (e.g. "aaa" → "aaaa"
at offset 1) recovered to the end via maximal prefix/suffix matching.
Root cause:
compositionstart never froze an edit snapshot while composing beforeinput was
skipped, so compositionend had no mid-edit baseline. recoverSelectionAfterEdit
also trusted a longest common prefix that claims the whole string when the
inserted run matches surrounding characters.
Fix:
Snapshot model text and caret on compositionstart; keep that baseline through
composition. Reconstruct post-edit carets with a selection-anchored head/middle/
tail splice before falling back to prefix/suffix, and prefer that recovery when
a live selection is parked at the end after a mid-text edit. Cover both edges
in rich-composer-selection tests.
Verification:
- pnpm --dir desktop/frontend exec tsx src/__tests__/rich-composer-selection.test.tsx
- pnpm --dir desktop/frontend exec tsx src/__tests__/composer-goal-toggle.test.tsx
- pnpm --dir desktop/frontend typecheck
* fix(desktop): preserve late WebView2 IME commits
Problem:
Windows WebView2 can dispatch compositionend before the committed IME DOM and final non-composing input are visible. Synchronizing the controlled model at compositionend publishes the stale pre-composition text and erases the selected candidate.
Root cause:
The composer assumed every Chromium host exposed the committed DOM by compositionend. Deferring every compositionend avoided the WebView2 blackout but regressed browsers and submit paths that require the committed text synchronously.
Fix:
Keep the pre-composition snapshot, synchronize immediately when compositionend already exposes changed DOM, and defer only while the DOM still matches the snapshot. Let the following non-composing input own late WebView2 commits, with a next-frame fallback and cleanup cancellation. Cover both event orders explicitly.
Verification:
- rich-composer-selection: 80 passed
- composer-goal-toggle: 171 passed
- pnpm --dir desktop/frontend test
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend build
- git diff --check
* fix(desktop): sync provider model state
* fix(desktop): preserve provider state boundaries
Problem: The source PR made per-session model switches rewrite the global default model and silently disabled existing provider balance endpoints.
Root cause: Session-local model state and user-global defaults were treated as one setting, while the stale-balance race fix was coupled to an unrelated provider capability restriction.
Fix: Keep hot model switches tab-local, retain existing balance_url behavior, and add regression coverage proving the global default and sibling tabs remain unchanged.
Verification: go test ./... (desktop); pnpm test; pnpm build; focused config/frontend tests; ./scripts/cache-guard.sh; git diff --check.
Co-authored-by: par73e <[email protected]>
* fix(desktop): serialize provider model switches
Problem:
Rapid model selections could let an older completion repopulate stale wallet state, while no-op or failed switches discarded the last known balance.
Root cause:
Balance request generations did not order SetModelForTab calls or distinguish stale model-switch completions. The current model also remained actionable without accounting for an in-flight switch.
Fix:
Serialize model switches per tab, track switch ownership and successful backend transitions, restore cached balance only when safe, ignore failed balance confirmations, make settled current-model selection a no-op, and invalidate provider state when tabs are discarded.
Verification:
- pnpm test
- pnpm build
- go test ./...
- go test -race . -run TestSetModelForTab -count=1
- ./scripts/cache-guard.sh
* fix(desktop): reconcile provider state across races
Problem:
Queued model fallback, cross-tab pending picks, and failed tab closes could leave provider UI state stale or trigger unnecessary switches.
Root cause:
Failure reconciliation omitted model metadata, pending pick ownership was component-global, and close invalidation ran before backend success.
Fix:
Refresh metadata after an older queued success, scope pending picks per tab, and invalidate provider generations only after a successful close. Add regression coverage for all three overlaps.
Verification:
pnpm test
pnpm build
pnpm exec tsc --noEmit -p tsconfig.test.json
go test -race . -run TestSetModelForTab -count=1
* Fix failed model switch reconciliation
Problem:
Rapid queued model switches could lose the pre-switch balance when every rebuild failed, and a failed selection could remain optimistically marked as current in the model picker.
Root cause:
Each queued attempt captured the already-cleared balance independently, while the picker did not receive or sequence the controller switch outcome.
Fix:
Keep one fallback balance for each per-tab switch queue, return an explicit switch result through App and Composer, and roll back plus reload the catalog only for the latest failed pick. Invalidate pre-click catalog requests so stale results cannot overwrite a retry.
Verification:
- pnpm test:all
- pnpm build
- go test ./...
- go vet ./...
- cd desktop && go test ./...
- cd desktop && go test -race ./...
- ./scripts/cache-guard.sh
- git diff --check
---------
Co-authored-by: par73e <[email protected]>
Co-authored-by: par73e <[email protected]>
* fix(desktop): support authorized Debian package updates
Problem:
Ubuntu users who installed the desktop .deb could not update in app because the updater could not replace root-owned system files.
Root cause:
Linux installs were treated uniformly as writable portable layouts, and the release manifest exposed only the tarball updater artifact.
Fix:
Detect dpkg-owned installs, publish signed native .deb metadata, and install upgrades through a packaged Polkit helper that re-verifies ownership, signatures, package identity, architecture, and version before delegating to apt. Preserve the portable tarball path and add user-facing progress, metrics, tests, and recovery documentation.
Verification:
- cd desktop && go test ./...
- cd desktop && go vet ./...
- pnpm --dir desktop/frontend test:typecheck
- pnpm --dir desktop/frontend test:updater
- pnpm --dir workers/crash-report typecheck
- pnpm --dir workers/crash-report test
- Linux amd64 helper test binary cross-compilation
- git diff --check
* fix(desktop): recover from updater mode changes
Problem:
The desktop CI failed on two unused updater helpers, and the UI could remain stuck in downloading or authorizing when install mode or manifest availability changed between updater steps.
Root cause:
The backend opened the manual download page and returned nil after reclassification, while the frontend treated nil as an in-flight operation with no terminal state. Two obsolete helper functions also remained after the refactor.
Fix:
Return and emit a typed manual-update error before opening the download page, preserve the manual fallback action in the shared updater state, add authorization/install/cancellation/reclassification coverage, and remove the unused functions.
Verification:
- cd desktop && go test ./...
- cd desktop && go vet ./...
- pnpm --dir desktop/frontend test:typecheck
- pnpm --dir desktop/frontend test:updater
- pnpm --dir desktop/frontend build
- pnpm --dir workers/crash-report typecheck
- pnpm --dir workers/crash-report test
- Linux amd64 update-helper test binary and production build
- git diff --check
* Fix Linux package smoke checks under pipefail
Problem:
Linux .deb contract smoke checks could fail on valid packages when the producer emitted more data after grep found its match.
Root cause:
The build script combined set -o pipefail with grep -q. Early grep exit could close the pipe and make dpkg-deb terminate with SIGPIPE, turning a successful contract match into a failed pipeline.
Fix:
Consume the complete dpkg-deb output with non-quiet grep redirected to /dev/null, and add a packaging guard that rejects the unsafe early-exit patterns.
Verification:
- cd desktop && go test ./...
- cd desktop && go test -run TestDesktopPackagesPreserveNativePlatformLaunchers -count=1 .
- pnpm --dir desktop/frontend test:updater
- bash -n scripts/desktop-build.sh
- git diff --check
* fix(auto): scope recovery failures to exact operations
Problem:
Auto Guard treated consecutive execution failures and reviewer rejections as task-wide state. After repeated test failures, unrelated workspace writes could remain blocked, and revising the proposal or starting a new ordinary turn did not reliably clear the latch.
Root cause:
Failure and reviewer counters were keyed only by the root task. Recovery observations carried no turn scope, revise kept the active failure, and the display surfaced only a generic Auto Guard error.
Fix:
Scope failure budgets to the exact operation and recovery scope, rotate episodes for different operations and ordinary turns, clear them on revise or when leaving Auto, retain stable Goal scopes across recovery persistence, scope reviewer rejection budgets to each proposal, and surface actionable blocker text. Update the English and Chinese approval-mode documentation and add compatibility coverage.
Verification:
- env -u DEEPSEEK_API_KEY go test ./... -count=1
- go test -race ./internal/recovery ./internal/control -count=1
- cd desktop && go test . -run 'Recovery|AutoGuard' -count=1
- ./scripts/cache-guard.sh
- go vet ./internal/recovery ./internal/agent ./internal/control
* fix(plan): add explicit non-executing exit
Problem:
Plan approval offered execution or revision, but no direct way to leave Plan without running the proposal. Rejecting the prompt kept Plan active, so desktop and CLI users could become stuck in the planning workflow.
Root cause:
The desktop already had an exit callback but did not expose it as a decision. The CLI represented Plan as a two-choice boolean approval and only cleared Plan after approval.
Fix:
Add a third exit-without-executing action on desktop and CLI, switch both UI and controller state out of Plan before rejecting that action, preserve revise and n/Esc keep-planning behavior, localize all supported languages, update docs, and add click, keyboard, duplicate-submit, banner, and state-sync coverage.
Verification:
- pnpm test
- pnpm test:typecheck
- pnpm typecheck
- pnpm check:css
- pnpm build
- env -u DEEPSEEK_API_KEY go test ./... -count=1
- go vet ./internal/cli ./internal/i18n
- git diff --check
* docs(plan): clarify cross-client shortcuts
Problem:
The approval guide described Desktop click behavior and Esc handling as if they applied identically to the CLI, leaving the new CLI Plan decision ambiguous.
Root cause:
The Plan action documentation was written before the CLI received its explicit three-action workflow.
Fix:
Document Desktop and CLI activation separately, state that CLI n/Esc keeps planning during a pending Plan confirmation, and scope the generic Esc stop behavior accordingly in English and Chinese.
Verification:
- git diff --check
* test(plan): pin localized three-action contract
Problem:
The CLI Plan workflow depends on three numbered actions in every supported locale, but tests only checked the English labels and non-empty catalog fields.
Root cause:
Localization completeness did not assert the semantic action count or the execute, revise, and exit-without-executing labels.
Fix:
Add a catalog-level regression test that requires exactly three numbered Plan actions and the expected semantic labels in English, Simplified Chinese, and Traditional Chinese.
Verification:
- go test ./internal/i18n -count=1
- git diff --check
* fix(auto): bound recovery by execution episode
Problem:
Auto recovery could still feel like a sticky lock: repeated Maven/test failures
blocked progress, different parameter retries could avoid the same-operation
cap, and restart or mode hydration risked re-arming temporary failure state.
Users needed a low-friction path where this turn auto-recovers with a hard
ceiling, then the next turn recovers automatically.
Root cause:
Failure and reviewer budgets mixed TaskScope with temporary execution state,
kept a single active failure rather than Episode-scoped counters, re-armed
locks from sidecar snapshots, and lacked a generation barrier for late tool
results after mode switches. There was no turn-level stop with a summarize-only
finalization path or a wire-compatible recovery_paused outcome.
Fix:
- Separate host-owned EpisodeID (failure/reviewer/stop budgets) from TaskScopeID
(Goal and explicit task grants).
- Fixed budgets with no settings: 3 identical operation failures, 6 Episode
execution failures since last real progress, 3 cumulative reviewer rejects,
3 re-submissions of stopped operations.
- Successful mutation/verification clears no-progress budgets; diagnostic reads
do not. Provider/cancel/user-reject/pre-exec blocks stay out of the budget.
- Gate generation invalidates stale observations; real mode changes rotate
Episode and dismiss recovery waiters without approving mutations. Same-value
mode replays are no-ops.
- Episode stop blocks remaining tools, offers one finalization round, then
returns RecoveryPauseError with turn_done.outcome=recovery_paused (info UI,
not send-failed).
- Persistence writes last_failure evidence only; restore never re-arms locks.
- Content-free metrics for stops, mode resets, and stale observations.
Verification:
- go test ./internal/recovery ./internal/agent ./internal/control ./internal/eventwire ./internal/cli -count=1
- go test -race ./internal/recovery ./internal/agent ./internal/control -count=1
- pnpm exec tsx desktop/frontend/src/__tests__/send-failed.test.ts
- ./scripts/cache-guard.sh
- go vet ./internal/recovery ./internal/agent ./internal/control ./internal/event
- git diff --check
* style(auto): format episode recovery changes
Problem:
The Episode recovery commit left three root-module Go files outside gofmt canonical formatting, causing the macOS and Ubuntu CI jobs to stop before vet, build, and tests.
Root cause:
The final Episode changes were committed without applying gofmt to all touched Go files.
Fix:
Run gofmt on the three files reported by the same command used in CI. The resulting diff is whitespace-only.
Verification:
- gofmt -l . excluding the independent desktop module returns no files
- git diff -w --exit-code
- git diff --check
* fix(auto): close Episode budget bypasses and lint gaps
Problem:
Episode hard limits could still be bypassed three ways: a successful
recovery summary returned nil so Goal auto-continue opened another Run
with a fresh finalization round; total failure and reviewer budgets were
per TaskID so each new sub-agent got a full ceiling; and a reviewer
Continue cleared cumulative rejects before any real mutation progress.
golangci-lint also failed on empty branches and unused helpers.
Root cause:
Finalization success treated the pause as a normal turn end; Episode
totals lived on taskRuntime; reviewRejects reset on Continue; dead
helpers and empty branches remained from the Episode refactor.
Fix:
- Return RecoveryPauseError after a successful summarize-only finalization
so turn_done reports recovery_paused and Goal auto-continue stops.
- Keep exact-operation counters task-scoped; move Episode totals,
reviewer rejects, stopped-op retries, hard stop, and finalization to a
Gate-level episode budget shared by root and all sub-agents.
- Clear reviewer rejects only on real mutation/verification success, a
new Episode, or revise — not on reviewer Continue or human continue.
- Remove unused helpers/empty branches and add regressions for shared
sub-agent budgets and reject→continue→reject cumulative counting.
- Block Goal on RecoveryPauseError like FinalReadinessError.
Verification:
- go test ./internal/recovery ./internal/agent ./internal/control -count=1
- go test -race ./internal/recovery ./internal/agent ./internal/control -count=1
- go vet ./internal/recovery ./internal/agent ./internal/control
- gofmt clean on touched packages
- git diff --check
* fix(auto): keep Goal running after recovery pause
Problem:
RecoveryPauseError set the Goal to blocked. The next ordinary user message
opened a fresh Episode but lost the active Goal prompt and delivery scope, so
replying continue did not actually resume Goal work without an explicit resume.
Root cause:
goalShouldBlockOnError treated RecoveryPauseError like FinalReadinessError and
called stopGoal(GoalStatusBlocked). Only running Goals reinject model context
and delivery scope.
Fix:
Keep the Goal running after RecoveryPauseError so auto-continue stops but the
next real user turn reuses the same Goal and delivery scope. Reserve
GoalStatusBlocked for FinalReadinessError. Add a regression covering pause then
ordinary continue.
Verification:
- go test ./internal/control -run 'RecoveryPause|GoalReadiness' -count=1
- go test ./internal/control -count=1
* fix(recovery): present paused turns consistently
Problem:
Recovery pauses were informational in the Desktop React chat, but the Desktop host, bot bridge, Serve WebUI, TUI, metrics, and headless CLI still treated them as failures.
Root cause:
Those consumers classified turn completion from a non-nil error alone and ignored the recovery_paused outcome. The headless CLI also had separate result and exit-code classification paths.
Fix:
Honor recovery_paused across every client surface, keep ordinary errors unchanged, and centralize headless completion classification so notifications, JSON output, user messaging, and exit codes agree.
Verification:
go test ./internal/cli ./internal/serve ./internal/control ./internal/eventwire ./internal/agent ./internal/recovery -count=1
go test -race ./internal/cli ./internal/control -count=1
cd desktop && go test ./... -count=1
cd desktop && go vet ./...
pnpm exec tsx src/__tests__/send-failed.test.ts
./scripts/cache-guard.sh
go vet ./internal/cli ./internal/serve ./internal/control ./internal/event ./internal/eventwire
git diff --check
* feat(desktop): expand highlight.js language set to 52 languages
Register 26 additional languages (from 26 to 52) covering common
ecosystems: lua, dart, groovy, objectivec, r, perl, fsharp, haskell,
elixir, clojure, erlang, julia, scss, less, latex, vim, nginx, apache,
protobuf, graphql, cmake, gradle, properties, coffeescript, matlab,
and vbnet.
Map XAML (.xaml) to XML highlighting since highlight.js has no native
XAML lexer and XAML is an XML dialect.
Extend EXT, ALIASES (lang.ts) and byExt (selectedTextContext.ts) tables
with corresponding file-extension and fence-tag aliases so all three
code-preview paths — Markdown fenced blocks, tool diffs, and the
workspace file viewer — resolve the correct language.
Closes#6825 (desktop portion).
* fix(desktop): correct jsx/tsx languageFor mappings to registered hljs names
languageFor() in selectedTextContext.ts mapped .jsx and .tsx to "jsx" and
"tsx", but highlight.js does not register languages under those names --
the actual registered names are "javascript" and "typescript". While the
immediate consumer (formatSelectionReference) works correctly because the
downstream highlightToHtml() passes through resolveLang() which handles
these aliases, the mapping is technically wrong and would break for any
future consumer that uses the languageFor return value directly without
alias resolution.
Fix: map jsx to "javascript" and tsx to "typescript" to match the
highlight.js registration names, making the function self-consistent.
* fix(desktop): unify syntax highlight language mapping
Problem:
- File previews and tool diffs could disagree for special filenames such as Dockerfile, Makefile, CMakeLists.txt, and .htaccess.
- Plan-revision fences replaced semantic tags such as TSX, HTML, TOML, and XAML with fallback lexer names.
Root cause:
- Path-to-language behavior was duplicated across independent tables, and extension-only matching could not recognize extensionless build and server configuration files.
Fix:
- Centralize semantic path mapping for both slash styles, extensions, and special basenames.
- Resolve semantic tags to highlight.js lexers only at the rendering boundary.
- Add regression coverage for mapped lexers, special filenames, UI consistency, and provider-visible fence tags.
Verification:
- pnpm test:typecheck
- pnpm test
- pnpm build
- ./scripts/cache-guard.sh
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* test(desktop): cover remaining special language paths
Problem:
- The shared path resolver supported httpd.conf and .makefile suffixes, but the focused path matrix did not exercise those branches directly.
Root cause:
- The initial regression matrix covered the lexer set and primary special basenames without one concrete path for each remaining matcher.
Fix:
- Add end-to-end path, tool-diff, file-preview, and lexer assertions for httpd.conf and module.makefile.
Verification:
- pnpm exec tsx src/__tests__/language-highlighting.test.ts
- pnpm test:typecheck
- git diff --check
* fix(desktop): cap chat syntax highlighting
Problem:
- Ordinary chat and tool code blocks always ran highlight.js, even when a block exceeded the workspace preview's 512 KiB or 20,000-line safety budget.
Root cause:
- The highlighting budget lived inside the line-numbered workspace viewer instead of the shared highlighting layer.
Fix:
- Move the byte and line limits into the shared highlighter.
- Measure ordinary blocks using UTF-8 bytes without allocating a duplicate buffer.
- Fall back to escaped plain text above either limit while preserving content and copy behavior.
- Reuse the same helper from workspace and ordinary code viewers.
Verification:
- pnpm exec tsx src/__tests__/line-number-code.test.tsx
- pnpm exec tsx src/__tests__/language-highlighting.test.ts
- pnpm test:typecheck
- pnpm test
- pnpm build
- ./scripts/cache-guard.sh
- git diff --check
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* fix: plan mode focus stealing - stabilize onRevisionActiveChange and avoid unnecessary re-renders
* fix(desktop): preserve transcript selection during plan revision
Problem:
The open plan-revision textarea reclaimed focus whenever its parent rerendered, clearing transcript text selections before users could copy them.
Root cause:
The focus effect depended on a callback prop whose identity could change during periodic App refreshes, so every callback change called focus again. The original patch also relied on suppressing tab metadata rerenders instead of making the approval owner resilient.
Fix:
Keep the App callback stable, focus the revision textarea only when revisionOpen changes, track the latest callback for close and unmount cleanup, remove the unrelated JSON.stringify polling dedupe, and add a regression that churns the callback while a transcript selection is active.
Verification:
- pnpm test
- pnpm test:typecheck
- pnpm typecheck
- ./scripts/cache-guard.sh
- Browser smoke test at http://127.0.0.1:5173/
* test(desktop): document plan revision rerender regression
Problem:
The focus regression test simulated callback identity churn without documenting why that is the relevant production trigger.
Root cause:
Periodic tab metadata refreshes rerender App, which historically recreated the revision-active callback and refocused the textarea.
Fix:
Document the parent-rerender trigger next to the regression step.
Verification:
- pnpm exec tsx src/__tests__/approval-modal-file-reference.test.tsx
- git diff --check
---------
Co-authored-by: SauronSkywalker <[email protected]>
Co-authored-by: SivanCola <[email protected]>
* fix: add review evidence for complete_step
* fix: accept dedicated review tool receipts
* fix(evidence): require fresh review receipts
Problem:
complete_step accepted any successful review/task receipt in the turn, including a review that predated a later mutation or a background task that had only started.
Root cause:
The review evidence matcher scanned the whole ledger without applying mutation ordering, changed-path coverage, or the structured review_report contract.
Fix:
Require review evidence to be fresh for the latest mutation and cover its changed result. Accept collected structured review reports, reject background starts, and define review evidence as activity completion while leaving blocking verdict enforcement to the delivery gate.
Verification:
- go test ./internal/evidence ./internal/tool/builtin -count=1
- ./scripts/cache-guard.sh
- go vet ./...
- go test ./internal/plugin -run TestLazyCacheMissAsyncSpawn -count=1
- env -u DEEPSEEK_API_KEY go test ./internal/provider/openai -count=1
---------
Co-authored-by: SivanCola <[email protected]>
The scrollspy built its list of sections from the sidebar links, which are
grouped by topic rather than page order, then picked the last section
scrolled past as active. That assumes the list is in document order, so the
highlight settled on the wrong item. Sort the sections by document position.
Clicking a link also swept the highlight through every section it passed,
because the spy runs on each frame of the smooth scroll. Pin the highlight to
the clicked section and release it on scrollend, falling back to the plain
jump where scrollend isn't supported.
Set aria-current on the active link so assistive tech can announce it.
Co-authored-by: 9710willy <[email protected]>
Problem: Stable recovery publishers failed when an immutable candidate predated its reviewed release-note entry. A partially successful run also had no safe way to retry only failed channels.
Root cause: CLI and Desktop rendered notes from the candidate checkout, while preflight validated notes from the protected control plane. The orchestrator always invoked every publisher during recovery.
Fix: Upload the preflight-rendered notes and consume that exact artifact in orchestrated CLI/Desktop publishers. Add opt-out channel switches for manual recovery while retaining public postflight verification for skipped channels.
Verification: bash scripts/release-workflows.test.sh; node scripts/release-notes.mjs render --version v1.17.19 --output /tmp/reasonix-release-notes-v1.17.19.md; git diff --check.
* fix(plugin): drain lazy schema writes on close
Problem:
Formal release tests intermittently failed while removing plugin cache temp directories because a lazy schema cache writer was still running.
Root cause:
Lazy MCP startup launched SaveCachedSchema in an untracked goroutine, so Host.Close could return before the detached write finished.
Fix:
Register lazy schema persistence with the Host background-write WaitGroup and add a deterministic close-wait regression test.
Verification:
- focused lazy lifecycle tests, 50 runs
- go test ./internal/plugin -count=5
- go test -race ./internal/plugin
- two consecutive REASONIX_RELEASE_CACHE_GUARD=1 go test ./... -count=1
* fix(plugin): close remaining cache write races
Problem:
PR review found that the synchronous lazy cache-hit path could still write after Host.Close returned, while Windows CI intermittently observed the SSE concurrency assertion before its handler published the active count.
Root cause:
The cache-hit path ended its deferred-spawn lifecycle before persisting the schema, and the SSE test closed its started signal before updating maxPosts.
Fix:
Queue cache-hit schema persistence before ending the deferred spawn, and publish the SSE started signal only after the concurrency counters are visible.
Verification:
- focused lazy lifecycle tests, 50 runs
- SSE concurrency regression test, 5000 runs
- focused race tests, 20 runs
- go test -race ./internal/plugin
- go test ./internal/plugin -count=10
- REASONIX_RELEASE_CACHE_GUARD=1 go test ./... -count=1
- go vet ./... && go build ./...
- Windows/amd64 plugin test cross-compilation
Problem:
The formal frontend test gate failed TypeScript checking before the 1.17.19 release.
Root cause:
The MCP install mock dereferenced optional Wails bindings without narrowing them, and an obsolete textarea helper remained after the test flow changed.
Fix:
Narrow the mocked App binding before delegation and remove the unused helper.
Verification:
- pnpm test:typecheck
- pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- pnpm test:all
- pnpm build
* fix(todo): add canonical todo state to MetaForTab so frontend reads authoritative server-side state
The frontend todoEntry previously only parsed the last todo_write tool call's args from state.items, which could show stale state when:
- Synthetic host-advance events from complete_step were not yet committed to items
- Session reload caused compactArchivedToolItems to truncate non-canonical todo args
Fix: add CanonicalTodos field to Meta struct, populate it from the Controller's authoritative todoState in MetaForTab, so the frontend always has access to the correct canonical task list independent of tool call event processing.
* fix(todo): frontend reads canonicalTodos from Meta for authoritative task list
The frontend todoEntry previously only parsed the last todo_write tool
call's args from state.items. Add fallback: if state.meta.canonicalTodos
(from MetaForTab) is available and non-empty, prefer it over parsing
tool call args. This ensures the frontend always shows the correct
canonical todo state even after session reload or when synthetic
host-advance events are not yet committed to items.
* fix: normalize app.go line endings to LF for gofmt compliance
* fix: gofmt alignment + types.ts Todo type import
- Fix gofmt alignment issue in Meta struct: CanonicalTodos field
tag alignment (extra spaces before backtick)
- Add import type { Todo } from './tools' to types.ts so the
canonicalTodos field in Meta interface can reference Todo[]
TypeScript type
* fix: add missing time import
* Revert "fix: add missing time import"
This reverts commit a21b24cadca43796933d520788cecacf4aac8657.
* fix(todo): preserve canonical state across session transitions
Problem:
Canonical todo status-only updates could be discarded by frontend meta equality, and session resets could retain stale todos. Empty canonical lists were also indistinguishable from an unavailable controller snapshot.
Root cause:
The Meta contract used omitempty on a slice, sameMeta ignored canonicalTodos, optimistic tab metadata dropped it, and session resume or rewind paths did not refresh authoritative todo metadata after resetting transcript state.
Fix:
Preserve unavailable, empty, and non-empty todo states across the Wails boundary; compare canonical todo content in meta equality; clear session-scoped todo metadata on reset; refresh it after session transitions; and add focused wire, reducer, visibility, new-session, and resume regression coverage.
Verification:
- cd desktop && go test ./...
- pnpm --dir desktop/frontend test
- pnpm --dir desktop/frontend build
- pnpm --dir desktop/frontend test:typecheck
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* fix(todo): guard canonical state across runtimes
Problem:
Remote workbench sessions dropped canonical Todos, and a Meta response started before a session transition could restore the previous session's Todo state after the reset.
Root cause:
Frontend Meta reads had no per-tab latest-request sequencing tied to session generations, while the native workbench projection ignored SessionSnapshot.Todos.
Fix:
Centralize guarded Meta reads, invalidate them on every session transition, project validated remote Todos with unavailable/empty/value semantics, and cover both race and remote projection paths.
Verification:
- go test ./...
- pnpm test
- pnpm typecheck
- pnpm test:typecheck
- pnpm build
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* fix(plugin): MCP stdio subprocess CWD falls back to WorkspaceRoot
When Spec.Dir is empty (the default for user-configured plugins), the
stdio subprocess inherits the parent process CWD. In the Wails desktop
app running multiple projects, that CWD is the first project listed in
desktop-projects.json, not the directory containing .mcp.json. Relative
config paths (e.g. --config-file ssh-config.json) then resolve against
the wrong project root.
Fix: newStdioTransport now falls back to Spec.WorkspaceRoot when Dir is
empty, so every project-scoped MCP server resolves relative paths
against the correct project root.
Fixes#6778.
* fix(plugin): align project MCP cwd authorization
Problem: Project-scoped stdio MCP cwd fallback could authorize a different relative executable than the one launched, while also changing cwd for user-installed servers.
Root cause: Launch and authorization resolved relative commands against different directories, and WorkspaceRoot was treated as process cwd without checking project provenance.
Fix: Centralize stdio working-directory selection, limit the fallback to repository-declared servers, resolve relative executables against that directory before hashing and launching, and bind the directory to the launch identity.
Verification: go test ./internal/plugin -count=1; go test ./internal/boot -count=1; env -u DEEPSEEK_API_KEY go test ./...; go vet ./...; Windows and Linux test cross-compilation; git diff --check.
---------
Co-authored-by: SivanCola <[email protected]>
* fix(mcp): make trusted servers zero-config and reliable
Run authorized stdio servers on the host by default while retaining private state directories, bounded lifecycle cleanup, lazy schema restoration, and single-flight startup.
Add durable activation, transactional readiness and rollback, CLI candidate probing, and inherited MCP access for child agents. Include a real chrome-devtools-mcp release smoke test.
* feat(desktop): reduce MCP install to one input
Accept a command, URL, or JSON definition in the common install path, surface structured readiness and authentication states, and keep transport and timeout controls in the advanced edit flow.
* feat(desktop): unify MCP quick and advanced setup
Problem: The zero-config MCP installer removed useful configuration controls and left the add-server page feeling too sparse.
Design: Keep quick install as the default, restore manual and JSON modes in the same editor, carry pasted definitions into advanced modes, and make the automatic install outcomes explicit.
Verification: npm run typecheck; npx tsx src/__tests__/capabilities-panel-actions.test.ts; npm run check:css; npm run build; git diff --check.
* fix(plugin): remove obsolete deferred wrapper
Problem: golangci-lint reported Host.addDeferred as an unused private method.
Root cause: Lazy startup now calls EnsureConnectedWithLifecycle directly, but the old wrapper remained after the single-flight migration.
Fix: Remove the dead wrapper and its stale comment without changing runtime behavior.
Verification: go test ./internal/plugin; go test -race ./internal/plugin; scripts/check-cache-impact.sh; git diff --check.
* fix(desktop): remove obsolete MCP intent helper
Problem: The desktop golangci-lint job reported mcpStartIntent as an unused private function.
Root cause: Legacy startIntent fields are now derived in withPluginConfigInWorkspace, but the previous helper remained after the activation-state migration.
Fix: Remove the dead helper while preserving the derived compatibility fields.
Verification: cd desktop && go test ./...; cd desktop && go vet ./...; scripts/check-cache-impact.sh; git diff --check.
* fix(mcp): preserve trust and on-demand boundaries
Problem: Explicit subagent tool allowlists could be widened by MCP inheritance, host-session readiness probes owned successful stdio processes, durable enable still connected eagerly, activation updates could clobber each other, quick names followed trailing arguments, and Windows asserted POSIX mode bits.
Root cause: MCP inheritance bypassed the normal registry filter; the short probe context was reused as process lifetime; enable reused the live-connect API; activation stores only locked per instance; name derivation scanned from the command tail; and the permission test was not platform-aware.
Fix: Keep explicit allowlists authoritative while preserving default MCP inheritance, split ExtraPlugin call and lifetime contexts, add cache-backed on-demand registration, serialize activation transactions with a cross-process lock, derive quick names from launcher operands in desktop and CLI, and scope mode-bit assertions to POSIX.
Verification: go test -race ./internal/agent ./internal/boot ./internal/control ./internal/config; env -u DEEPSEEK_API_KEY go test ./...; cd desktop and run go test ./... plus go vet ./...; frontend MCP actions, typecheck, build, and CSS checks; go vet ./...; ./scripts/cache-guard.sh; git diff --check.
* fix(registry): re-review publisher updates
Problem:
Publishers could replace the source, manifest, or metadata of an active package while it stayed live and retained its verified badge.
Root cause:
Registry moderation was triggered only when the public package kind changed, although every accepted republish creates a new immutable package version.
Fix:
Return every non-admin package update to pending review, clear verification, and cover active, hidden, rejected, and trusted-admin update paths.
Verification:
- workers/crash-report: npm test (35 passed)
- workers/crash-report: npm run typecheck
- git diff --check
* fix(registry): fence approvals to reviewed revision
Problem:
A stale moderation page could approve a package version submitted after the reviewer loaded the queue.
Root cause:
Approval requests carried only the slug and action, while both admin backends performed unconditional status updates.
Fix:
Bind approvals to the reviewed version, update timestamp, and status with one atomic D1 conditional update. Stale or old-page requests now fail closed across both admin interfaces.
Verification:
- worker tests: 41 passed
- worker typecheck
- site tests: 26 passed
- site production build
- git diff --check
* feat(registry): support plugin submissions
Problem:
The community registry exposed only skill and MCP package kinds even though the installer already supports plugin packages.
Root cause:
Plugin was absent from the site form, query and publish schemas, shared package types, and republish persistence.
Fix:
Add plugin as a first-class registry kind, require installable GitHub plugin sources, persist kind changes on republish, and expose plugin filtering and guidance in the site and moderation UI.
Verification:
- npm test (workers/crash-report)
- npm run typecheck (workers/crash-report)
- npm test (site)
- npm run build (site)
- Browser verification at /skills/
* fix(registry): re-review package reclassifications
Problem:
Package owners could reclassify an active or verified package as a plugin without returning it to moderation, and the publish API accepted plugin installers hidden behind skill or MCP kinds.
Root cause:
Republish persisted kind without coupling the change to status and verification, while validation only checked installer mismatches when kind itself was plugin.
Fix:
Return publisher-initiated active kind changes to pending, clear verification on every non-admin kind change, and require explicit installer kinds to match the public package kind.
Verification:
- npm test (workers/crash-report): 29 passed
- npm run typecheck (workers/crash-report)
- git diff --check
* fix(registry): disclose compatible plugin formats
Problem:
The registry publish form and API error claimed plugin submissions required a Reasonix-native manifest, even though the installer also accepts Codex and Claude plugin packages.
Root cause:
The submission guidance was written for the first native manifest path and was not updated when compatibility parsers and Claude marketplace support were added.
Fix:
List the native, Codex, Claude, and supported Claude marketplace manifests in both the form guidance and server validation error, with contract tests covering every format.
Verification:
- worker test suite: 30 tests passed
- worker TypeScript typecheck passed
- site test suite: 24 tests passed
- Astro production build passed
- git diff --check passed
* fix(registry): pin installs to declared package kind
Problem:
- Registry skill and MCP submissions could retain installKind=auto, while copied install requests omitted the declared kind.
Root cause:
- Only plugin submissions normalized auto, and the API echoed legacy installer values independently of the public package kind.
Fix:
- Normalize every submission and API response to the declared kind, and preserve that kind in publish and copy flows.
Verification:
- workers/crash-report: npm test; npm run typecheck
- site: npm test; npm run build
- git diff --check
* fix(registry): harden plugin source validation
Problem:
Plugin submissions could preserve control characters in copied install prompts, and non-canonical GitHub URLs could select paths outside the cloned repository.
Root cause:
The registry accepted any GitHub URL with two path segments while the Go installer ignored unsupported URL tails and joined tree paths without a containment check.
Fix:
Reject unsafe source characters and non-repository GitHub shapes, make the shared Go parser fail closed, and verify selected plugin roots remain inside the resolved clone.
Verification:
go test ./internal/installsource
env -u DEEPSEEK_API_KEY go test ./...
go vet ./...
npm test && npm run typecheck (workers/crash-report)
npm test && npm run build (site)
* fix(desktop): keep theme editor actions accessible
Problem:
- The theme editor could hide its Save and Apply actions inside the settings layout on Windows and compress controls in Creation mode.
- The default code-viewer contract could race React 19 lazy resolution.
Root cause:
- The fixed editor overlay remained inside a containing block and responded to the global viewport instead of its own available width.
- The code-viewer assertion assumed one task flush was enough for Suspense to resolve.
Fix:
- Portal the editor to the document body, retain accessible modal focus behavior, and use editor-scoped container queries with stable action placement.
- Let segmented controls shrink and wrap at narrow editor widths.
- Wait for the lazy default code viewer before asserting its DOM contract.
Verification:
- pnpm test
- pnpm build
- Browser validation at the reported Windows flow and a 957x675 viewport
* fix(desktop): contain theme editor escape handling
Problem:
Saving a theme left Escape unowned by the nested editor, allowing the outer Settings modal to close. Safe-area segmented controls also used a narrower selector than the related contribution in #6726.
Root cause:
The busy branch returned before preventing propagation, and the document listener captured stale busy and cancel closures. The segmented button rule was scoped only to direct .set-seg children.
Fix:
Always consume Escape while the editor is mounted, read live busy and cancel values through refs, and add a runtime modal-ownership regression. Adopt the unified setting-row selector from #6726.
Verification:
- pnpm typecheck
- pnpm test:typecheck
- pnpm check:css
- pnpm test
- pnpm build
- git diff --check
Co-authored-by: HaoyueQin <[email protected]>
---------
Co-authored-by: HaoyueQin <[email protected]>
Problem:
MiniMax content-review failures 1026 and 1027 arrive as HTTP 422, so Reasonix mislabeled them as invalid parameters and discarded the provider trace identifier.
Root cause:
Provider errors were classified only by HTTP status, and APIError retained the response body but not trace_id response headers.
Fix:
Recognize MiniMax input/output content-review responses, show localized recovery guidance without blindly retrying 422 requests, parse MiniMax base_resp messages, and preserve bounded trace IDs for diagnostics.
Verification:
- go test ./internal/provider ./internal/control ./internal/i18n
- env -u DEEPSEEK_API_KEY go test ./...
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check
* feat: show selected text card in chat bubble with formatted preview
Display the selected chat/code text as an expandable card in the sent
message, reusing the existing paste-block mechanism. The input-area
tooltip now renders with Markdown for syntax-highlighted preview.
Changes:
- selectedTextContext.ts: add SELECTION_LABEL_RE and formatSelectionLabel
- Composer.tsx: add selection labels to displayRefs, begin/end markers,
and Markdown tooltip for input cards
- ComposerContextCard.tsx: tooltipLabel type string -> ReactNode
- Message.tsx: extend PASTE_LABEL_RE to match selection labels, use
Markdown for expanded paste/selection content
- styles.css: .msg-pasted-expanded .md white-space, .tooltip .md pre
- test: update display assertion for new label format
* fix(desktop): keep selected context cards display-only
Problem:
Selected-text cards duplicated quoted content into provider-visible submit bytes, used truncated labels as unique identities, and rendered existing pasted blocks as Markdown.
Root cause:
The card UI reused pasted-text begin/end markers stored inside submitText instead of reading the selected-context payload that was already persisted for the turn.
Fix:
Recover selection cards from the existing safe JSON payload, correlate duplicate labels by occurrence, sanitize file-name brackets, render code with the highlighted code viewer, and keep pasted blocks literal.
Verification:
- pnpm test
- pnpm build
- pnpm typecheck
- pnpm test:typecheck
- ./scripts/cache-guard.sh
- Browser-verified duplicate chat cards, bracketed file names, code indentation/highlighting, Markdown chat content, and literal pasted text
Co-authored-by: SivanCola <[email protected]>
* fix(desktop): preserve selected context through edits
Problem:
Editing a message with selected-context cards removed the quoted context while leaving inert labels, and label-like prose could be consumed as a card.
Root cause:
Display labels were parsed with a global regex and edit replay treated the selected-context suffix as part of the visible body.
Fix:
Derive exact label suffixes from validated JSON metadata, isolate generated labels from editable text, and split/reappend the selected-context block during replay.
Verification:
- targeted selected-context and edit-replay tests
- pnpm build
- pnpm test:typecheck
- ./scripts/cache-guard.sh
- browser-verified selected-context edit preservation
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SauronSkywalker <[email protected]>
Co-authored-by: SivanCola <[email protected]>
* feat(auto): reserve prompts for plan decisions
Problem:
Auto Guard interrupted ordinary execution based on inferred risk, while material plan changes did not present a focused, neutral decision surface.
Root cause:
Recovery policy mixed execution-risk classification with user-owned strategy and scope choices, and recovery events did not carry enough plan context for clients to show the transition.
Fix:
Keep ordinary Auto execution under existing permission and sandbox policy, review structural plan transitions independently, stop repeated technical failures without asking for risk consent, and present optional before/after plan deltas across Desktop, TUI, and bot clients.
Verification:
- go test ./internal/eventwire ./internal/recovery ./internal/agent ./internal/control ./internal/cli ./internal/bot -count=1
- pnpm typecheck
- pnpm test:typecheck
- pnpm exec tsx src/__tests__/decision-surface.test.tsx
- pnpm check:css
- go run ./cmd/remote-protocol-gen -root . -check
- ./scripts/cache-guard.sh
* Fix Auto plan decision follow-through
Problem:
Adopting an Auto plan transition could still fail todo continuity, Plan plus Auto could ask twice for the same plan, and the CLI preselected the adopt choice.
Root cause:
The recovery decision carried no one-call replacement authorization, plan-transition review ignored active Plan mode, and approval selection always started at the first row.
Fix:
Carry a host-only replacement authorization into the approved todo_write call, preserve completed task history, use the existing Plan exit gate during planning, and require an explicit CLI plan choice.
Verification:
Focused agent, recovery, todo, evidence, and CLI tests passed. go vet ./... passed. The cache guard and cache-impact check passed; the optional live provider probe passed with the invalid machine credential unset.
* fix(config): preserve permission policy when persisting approvals
Replace the allow-only section rewrite with a locked, strict, targeted mutation so concurrent approvals cannot drop existing policy fields or comments.\n\nRefs #2
* fix(config): respect TOML multiline strings in permission updates
Problem: Permission persistence could mistake section examples inside multiline strings for real config and report success after editing prompt text.
Root cause: The line-based section scanner did not preserve multiline basic or literal string state across lines, and candidate output was written without semantic validation.
Fix: Track TOML string forms while locating structural section and key lines, then require the candidate to parse and contain the exact requested allow rules before atomic replacement.
Verification: go test ./internal/config ./internal/boot ./internal/control; focused race tests for permission persistence; env -u DEEPSEEK_API_KEY go test ./...; go vet ./...; git diff --check.
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* fix(desktop): prevent Windows portable entrypoint collision
Problem:
Windows portable staging used Reasonix.exe for the GUI launcher and reasonix.exe for the bundled CLI. On case-insensitive Windows filesystems, the CLI silently overwrote the launcher, so desktop shortcuts opened the CLI.
Root cause:
The packaging contract was checked only as source strings and never validated against the final Windows staging directory.
Fix:
Give the internal Windows CLI sidecar the distinct reasonix-cli.exe name, update installer, updater, and remote lookup contracts, and verify the exact portable file set plus launcher identity before archiving.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- cd desktop && go test ./...
- cd desktop && go vet ./... && go build ./...
- go vet ./... && go build ./...
- Windows PE cross-build and portable verifier simulation
* fix(repair): accept renamed Windows CLI sidecar
Problem:
Windows desktop updates record reasonix-cli.exe in the pending release unit, but the repair validator rejects that renamed sidecar when the transaction is read back.
Root cause:
The packaging and updater contracts were updated to the collision-free filename without extending the repair transaction allowlist.
Fix:
Allow reasonix-cli.exe only as a non-primary release sibling and cover the complete Windows release unit through a PrepareFileUpdate-to-ReadPendingUpdate round trip.
Verification:
- go test ./internal/repair
- go test -race ./internal/repair
- go test ./...
- go vet ./...
- cd desktop && go test ./...
- git diff --check
Problem: ACP clients can discard available_commands_update when it arrives before a session-opening response, leaving slash commands unavailable after new, load, or resume.
Root cause: Session handlers emitted updates while their JSON-RPC request was still executing. Moving the send to a goroutine did not establish a wire-order guarantee.
Fix: Add a transport-owned post-response hook and route command advertisements for session/new, session/load, and session/resume through it while preserving transcript replay and config-rebuild ordering.
Verification: Focused wire-order tests repeated 10 times, ACP/control tests, ACP race tests, go vet, and the full Go suite passed.
Co-authored-by: RainbowXie <[email protected]>
Co-authored-by: ssfdust <[email protected]>
* fix(remote): harden workbench restore and detach
Problem:
- Physical Windows-to-Linux acceptance exposed null Git pages, a crash restoring sessions after provider catalog changes, and leaked attach/runtime processes after disconnect.
Root cause:
- Empty pages cloned into nil slices, typed-nil controllers crossed an interface boundary, and runtime children shared the SSH session while proxy teardown waited indefinitely.
Fix:
- Preserve allocated empty result pages, normalize typed-nil controllers, detach and reap runtime children, and make proxy teardown bounded.
- Add regression coverage for clean Git pages, provider removal restore, child reaping, and blocked stdin shutdown.
Verification:
- go test ./... and desktop go test ./...
- go test -race ./... and desktop go test -race ./...
- go vet ./... and desktop go vet ./...
- pnpm frontend test:all and build
- cache guard, protocol generation check, six-platform CLI cross-build, and npm release dry run
* fix(deps): update x/text for GO-2026-5970
Problem:
- The release CI govulncheck gate detected reachable GO-2026-5970 in golang.org/x/text v0.38.0.
Root cause:
- Both the root and Desktop modules pinned the vulnerable x/text release used through proxy normalization.
Fix:
- Upgrade golang.org/x/text to v0.39.0 in both modules and refresh their checksums.
Verification:
- govulncheck ./... reports no reachable vulnerabilities in both modules.
- Root and Desktop full tests, vet, and race suites pass.
* feat(agent): add Auto-mode failure recovery checkpoint / 为自动模式增加失败恢复检查点
Problem:
In Auto mode, after a tool or verification failure the agent could silently
change strategy, expand write scope, or raise risk without a user decision.
Root cause:
There was only the Ask/Auto/YOLO permission axis and plan confirmation. No
host-side checkpoint blocked unconfirmed recovery mutations after failures.
Fix:
- Add a shared per-task recovery state machine (idle/diagnosing/awaiting/
approved_once) consulted before permission and write-lock acquisition.
- Deterministic rules auto-continue same-strategy verification retries and
force confirm for high-risk, scope, strategy, or consecutive failures.
- Independent recovery reviewer with isolated policy/session; fail closed.
- Fresh recovery approval cards (continue/revise/stop); never session-drain.
- Config/session defaults: new sessions on, pre-upgrade sessions off.
- Desktop composer toggle + settings default; Bot structured recovery UX;
content-free recovery metrics; reviewer/gate sidecars for cold restart.
- Docs: English + Chinese tool-approval modes.
Verification:
- go test ./internal/recovery/ ./internal/control/ ./internal/bot/ -run
'TestRecovery|TestCache|Approval' -count=1
- Cache-shape test: success-path system/tools hashes unchanged with gate on
- desktop package compiles (go test -c)
Cache-impact: low — success path keeps stable system prompt and tool schemas
byte-stable; only failure-path dynamic user-tail and an isolated reviewer
session add tokens.
Cache-guard: internal/recovery TestRecoveryWiringPreservesSuccessPathCacheShape
System-prompt-review: N/A for main agent (reviewer uses a separate fixed policy)
* fix(agent): harden auto recovery checkpoints
Problem:
Auto recovery could start a concurrent Agent.Run, wait forever in headless mode, duplicate revise feedback, and persist incomplete or orphaned recovery state.
Root cause:
Failure guidance was routed through Controller.Steer, headless detection relied on approval timeout, reviewer transcripts were treated as session sidecars, and recovery state lacked ordered atomic persistence and task-scoped replay semantics.
Fix:
Deliver guidance to the exact Agent.Run, use explicit headless mode, isolate root and subagent recovery identities, make recovery state atomic and session-owned, support cold-start decisions, and complete the Wails/mock bridge contract.
Verification:
go test ./...; go test -race ./...; root and desktop vet/lint; desktop tests; frontend typecheck/tests/build; real-browser Continue/Revise/Stop checks.
* style: format recovery checkpoint fields
Problem:
CI stopped before compilation because four recovery-related Go files from the original PR were not gofmt-clean.
Root cause:
Aligned struct fields were added without running the repository-wide formatter gate.
Fix:
Apply gofmt to the affected root and desktop files without changing behavior.
Verification:
The exact root and desktop gofmt checks from CI now produce no output.
* test(recovery): respect Windows file modes
Problem:
The Windows CI leg failed even though the recovery snapshot was written atomically and remained valid JSON.
Root cause:
The test expected POSIX 0600 permission bits from FileMode.Perm on Windows, where NTFS permissions are represented by inherited ACLs and Go reports synthetic 0666 bits.
Fix:
Keep the atomic persistence assertions on every platform and limit the numeric 0600 assertion to platforms where POSIX mode bits are meaningful.
Verification:
Focused recovery tests pass, the Windows test package cross-compiles, and the exact root gofmt gate is clean.
* fix: harden recovery checkpoint session isolation
Problem:
Recovery state could cross session boundaries, delayed snapshots could target a new session, destructive delete tools could bypass human confirmation, synchronous approval replies could be lost, and legacy sessions could resume enabled.
Root cause:
The gate resolved persistence paths asynchronously, session rotations did not consistently replace gate state, approval IDs were bound after event emission, delete tools were absent from deterministic risk rules, and resume ignored branch metadata.
Fix:
Capture and drain persistence per session, reset or carry recovery state explicitly across every rotation, bind approval IDs before emission, classify delete tools as high risk, and restore the persisted per-session preference with a legacy-off default.
Verification:
go test ./...
(cd desktop && go test ./...)
go test -race ./internal/recovery ./internal/control -run 'Recovery|FreshSessionRotations|SetSessionPath'
go vet ./...
./scripts/cache-guard.sh
* fix(recovery): preserve defaults across session resets
Problem:
Recovery settings were omitted from rendered configuration, and fresh sessions could inherit a resumed session preference instead of the configured default.
Root cause:
The serializers did not emit the new fields, while the controller kept one mutable recovery flag for both current-session state and future-session defaults. Desktop metadata snapshots also did not consistently mirror the effective preference.
Fix:
Render and round-trip the recovery settings, separate current and default controller state, route known-new paths through an explicit fresh-session binding, and synchronize Desktop tabs and branch metadata after rotation.
Verification:
go test ./...
(cd desktop && go test .)
go test -race ./internal/control -run recovery rotation cases
(cd desktop && go test -race . -run recovery session cases)
* feat(agent): evolve recovery checkpoint into Auto Guard
Problem:
Auto only applied the recovery checkpoint after failures, duplicated UI and config controls, and escalated every uncertain recovery directly to a human.
Root cause:
High-risk classification was scoped to active failure state, while persisted one-shot grants and recovery-specific Stop and replay branches made the flow stateful and fragmented.
Fix:
Add a deterministic pre-action Auto Guard, return reviewer denials to agents before three-strike human escalation, remove stale grant replay and recovery Stop, consolidate config and UI, and preserve legacy wire and config decoding.
Verification:
- go test ./...
- go test -race ./internal/recovery
- (cd desktop && go test ./...)
- (cd desktop/frontend && pnpm test:typecheck && pnpm test && pnpm build)
* test(recovery): synchronize prompt resolution
Problem:
Linux CI could schedule prompt-resolution goroutines before the gate re-keyed its provisional waiter, leaving two new tests blocked until the package timeout.
Root cause:
The tests resolved real approval IDs without first binding them, so they depended on scheduler timing even though production prompt emitters use BindApprovalID.
Fix:
Bind each approval ID synchronously inside the prompt callback before resolving the decision.
Verification:
- go test ./internal/recovery -run TestHighRiskMutationPromptsBeforeAnyFailure\|TestReviewerBlockReturnsReasonThenEscalates -count=100
- go test -race ./internal/recovery -run TestHighRiskMutationPromptsBeforeAnyFailure\|TestReviewerBlockReturnsReasonThenEscalates -count=100
* ci: restart stuck Windows runner
Problem:
The Windows test job stayed in progress well past its configured 10-minute step timeout and ignored both normal and force-cancel requests.
Root cause:
The hosted runner stopped reporting progress after entering the test step, preventing the workflow from reaching a rerunnable conclusion.
Fix:
Advance the PR head without file changes so the workflow concurrency policy replaces the stuck run with a fresh runner.
Verification:
The same head already passed Ubuntu, macOS, race, lint, coverage, CodeQL, cache-impact, and desktop Linux checks.
* fix(desktop): keep Yolo visible in composer
Problem:
The Auto Guard refinement hid Yolo from the composer unless it was already active, which made a supported approval mode less discoverable and left the three-column control partially empty.
Root cause:
The composer rendered the Yolo button conditionally and narrowed the approval control for a two-option default surface.
Fix:
Always render Ask, Auto, and Yolo, restore the three-option width, and align English/Chinese documentation and regression coverage with the direct selection flow.
Verification:
- pnpm exec tsx src/__tests__/composer-run-strip.test.tsx
- pnpm test:typecheck
- pnpm check:css
- pnpm test
- pnpm build
- Browser DOM/click verification for all three visible options
* fix(recovery): close Auto Guard evidence gaps
Problem:
Auto Guard could miss dependency/config deletes, moves, and common install or publish commands, while the recovery reviewer never received the read-only evidence gathered after a failure.
Root cause:
High-risk classification covered only a subset of file mutation tools and broad command names, and successful diagnostic observations were discarded before populating the task-local diagnosis state.
Fix:
Classify every built-in file mutation path and risky package-manager subcommand, keep ordinary verification commands on the fast path, and persist bounded, deduplicated, task-local diagnostic excerpts for the isolated reviewer. Mark dynamic reviewer evidence as untrusted.
Verification:
- go test -race ./internal/recovery -count=1
- env -u DEEPSEEK_API_KEY go test ./... -count=1
- go vet ./...
- cd desktop && go test ./... -count=1
- go test ./internal/recovery -run TestRecoveryWiringPreservesSuccessPathCacheShape -count=1
- ./scripts/cache-guard.sh
* fix(recovery): remove obsolete path helper
Problem:
The PR lint job rejected an unused pathFromArgs helper left behind after high-risk classification moved to multi-path extraction.
Root cause:
The compatibility wrapper had no remaining callers, while go vet does not report unused package-level functions.
Fix:
Remove the obsolete wrapper and retain pathsFromArgs as the single path extraction implementation.
Verification:
- go test ./internal/recovery -count=1
- go test -race ./internal/recovery -count=1
- go run github.com/golangci/golangci-lint/v2/cmd/[email protected].2 run --timeout=5m
* test(recovery): freeze Auto Guard behavior matrix
Problem:
Auto Guard routing is embedded in the Gate coordinator, so later
refactors risk changing product semantics without a focused oracle.
Fix:
Add a pure Decision engine skeleton and table-driven golden coverage for
the product matrix: ordinary Auto paths never call the reviewer, high
risk and scope/strategy/second-failure ask immediately, ambiguous
recovery reviews once, reject budget escalates on the third block, and
reviewer errors fail closed without burning reject credits.
Verification:
go test ./internal/recovery/
* refactor(recovery): pure decision core, collapsed state, provider reviewer
Problem:
Auto Guard mixed classification, routing, agent-based review, and multi-source
runtime state in one Gate, making later product simplification risky.
Root cause:
Phase/ConsecutiveFails/SafeRetryLeft/Pending lived as parallel truth, and the
reviewer spun a full agent.Agent with tools and compaction.
Fix:
- Route through a pure Decide engine (Classifier facts → allow/review/ask).
- Keep one taskRuntime + activeFailure; derive Phase and wire compatibility
fields on Snapshot; Restore still drops Pending/ApprovalID grants.
- Replace the agent reviewer with a bounded provider.Stream call
(temp 0, max 256 tokens, 8 KiB total, 4 KiB output cap, recovery-reviewer
usage source).
Verification:
go test ./internal/recovery/ ./internal/control/ -run 'Recovery|AutoGuard|recovery'
* refactor(desktop): one-click Auto recovery and remove Guard toggles
Problem:
Recovery cards still used select-then-confirm, and unreleased per-tab /
Settings Auto Guard toggles competed with Ask/Auto/YOLO as a fourth control.
Fix:
- Recovery card is one click: Continue this step / Try another approach,
optional guidance, details collapsed, localized reason by change_kind.
- Empty revise feedback is accepted; host injects a fixed agent guidance.
- Remove desktop Settings and Wails per-tab Auto Guard toggles; kill switch
remains [agent].auto_recovery_checkpoint only.
- Stop persisting RecoveryCheckpointEnabled on BranchMeta.
- Document guarded Auto semantics in EN/ZH mode docs.
Verification:
go test ./internal/recovery ./internal/control -run Recovery
desktop go test focused recovery/session cases
pnpm exec tsx src/__tests__/decision-surface.test.tsx
* fix(desktop): keep legacy Auto Guard Wails no-ops
Problem:
Removing the Wails methods broke frontend typecheck against generated
bindings that still list the old surfaces.
Fix:
Restore thin no-op / config-read Wails methods so older frontends and
generated bindings keep compiling, without reviving per-tab or Settings
Auto Guard toggles as product controls.
Verification:
pnpm typecheck
go test . -run AutoRecovery|RecoveryCheckpoint|NewSessionUsesFresh (desktop)
* fix(recovery): unblock legacy Approve, honor project kill switch, keep evidence JSON valid
Problem:
1. Pre-action high-risk recovery cards parked a live waiter without taskRuntime,
so Approve looked them up via Snapshot and never called ResolveRecovery —
old clients could hang until context cancel.
2. Desktop re-applied Auto Guard from user-only config after boot, ignoring
project [agent].auto_recovery_checkpoint = "off".
3. Reviewer evidence over 6 KiB was clipped mid-JSON, producing invalid payload.
Root cause:
Live waiter state was inferred from a persistence-shaped snapshot; desktop
overwrote the controller kill switch; field budgeting happened after marshal.
Fix:
- Gate.HasApproval(id) inspects the live waiter table; Approve routes through it.
- Remove desktop secondary SetRecoveryCheckpointEnabled / DefaultEnabled overrides.
- Budget optional evidence fields, then marshal; assert json.Valid and size.
- UTF-8-safe feedback clip at the 4 KiB host cap.
Verification:
go test -race ./internal/recovery ./internal/control
go test . -run AutoRecovery|NewSessionUsesFresh (desktop)
* fix(ci): clear Auto Guard lint failures and freeze project kill switch
Problem:
CI golangci-lint v2.12.2 failed on three issues introduced by the Auto Guard
refactor: an empty if (SA9003), a redundant explicit type (ST1023), and an
unused desktop helper (U1000).
Fix:
- Assert the real recovery approval id is cleared after legacy Approve.
- Use r := reviewer in the decision matrix helper.
- Remove unused currentTabRecoveryCheckpointEnabled.
- Add boot regression: global on + project off disables Auto Guard at Build
and after SetFreshSessionPath.
Verification:
go test ./internal/recovery ./internal/control ./internal/boot -run LegacyApprove|BehaviorMatrix|BuildHonorsProject|HasApproval
staticcheck -checks SA9003,ST1023,U1000 on touched packages
* test(desktop): freeze project Auto Guard kill switch
Problem:
The existing regression only exercised boot.Build and did not pass through the desktop runtime wiring that previously reapplied the user-global Auto Guard setting.
Root cause:
The test used a legacy-compatible config path and treated a same-controller session rotation as proof of desktop rebuild behavior, so the secondary desktop overrides were outside its coverage.
Fix:
Use the primary user config path and add a desktop integration regression that builds a project tab with global on/project off, then verifies initial configuration and a fresh-session rotation both remain disabled.
Verification:
go test ./...
go test -race ./internal/recovery ./internal/control ./internal/boot
(cd desktop && go test ./...)
golangci-lint v2.12.2 in root and desktop
./scripts/cache-guard.sh
* refactor(recovery): make Auto Guard intrinsic to Auto
Problem:
The short-lived auto_recovery_checkpoint kill switch duplicated Auto mode state across config, recovery decisions, controller sessions, and desktop tabs, increasing user and maintenance cost.
Root cause:
Auto Guard was introduced as an optional feature even though its decision matrix is part of Auto mode's product contract.
Fix:
Remove the config key and the full runtime enablement chain. Ignore retired TOML and session metadata fields, clean them on the next save, and retain only no-op Wails compatibility methods for older generated frontends. Update docs and regressions, and format files that blocked the prior CI run.
Verification:
- go test -race ./internal/recovery ./internal/control
- go test ./internal/agent ./internal/config ./internal/boot
- cd desktop && go test ./...
- cd desktop/frontend && pnpm typecheck && pnpm test:typecheck
- decision-surface and composer-run-strip tests
- ./scripts/cache-guard.sh
* test(boot): isolate Windows config directories
Problem:
The Windows root-module CI repeatedly loaded a test provider from an earlier case, retried example.invalid for 92 seconds, and then timed out.
Root cause:
isolateConfigHome redirected HOME and XDG_CONFIG_HOME but not the AppData and LocalAppData directories used by os.UserConfigDir and os.UserCacheDir on Windows.
Fix:
Redirect USERPROFILE, AppData, and LocalAppData into the same per-test temporary directory.
Verification:
- go test ./internal/boot
- repeated the affected Boot tests three times
- confirmed the Go standard library reads AppData and LocalAppData on Windows
* fix(desktop): prevent approval control overlap
Problem:
The three-option approval bar overlapped the adjacent model selector at compact composer widths.
Root cause:
The 760px container rule shrank the approval wrapper to 164-178px while its child mode bar retained a fixed 200-232px width.
Fix:
Allow the approval mode bar to shrink with its compact parent and add a CSS contract regression for the responsive geometry.
Verification:
- pnpm test
- pnpm build
- pnpm typecheck
- pnpm test:typecheck
- pnpm check:css
- ./scripts/cache-guard.sh
- in-app browser geometry at 1280px and 1182px viewport widths
* fix(recovery): close Auto Guard review gaps
Problem:
Auto Guard missed wrapped dependency and configuration mutations, while desktop telemetry replayed cumulative recovery counters on every completed turn.
Root cause:
The high-risk classifier recognized only narrow command and path shapes, and the desktop sampled controller totals rather than per-turn deltas.
Fix:
Harden wrapper and configuration mutation classification, add atomic recovery metric draining, and cover both regressions with focused tests.
Verification:
- go test ./...
- go test -race ./internal/recovery ./internal/control
- cd desktop && go test ./...
- go vet ./...
- ./scripts/cache-guard.sh
* refactor(recovery): make Auto interruption-light
Problem:
Auto asked users to understand recovery internals and interrupted reversible workspace work, while Plan required select-then-confirm.
Root cause:
The recovery classifier treated config and dependency edits, strategy changes, and the second failure as human boundaries, and the desktop surface exposed internal guard concepts.
Fix:
- reserve human prompts for deterministic hard boundaries or three failed attempts
- keep low-risk recovery moving when optional review is unavailable
- simplify Plan and recovery decisions to two direct, accessible buttons
- align desktop, bot, docs, and localized copy
Verification:
- go test ./...
- pnpm --dir desktop/frontend test:all
- pnpm --dir desktop/frontend build
- browser verification for Auto, Plan + Auto, Goal + Auto, Plan confirmation, and recovery confirmation
* fix(recovery): align Auto boundaries and reviewer
Problem:
Auto could silently allow destructive or external commands, while the isolated reviewer still rejected bounded project-local dependency, config, strategy, and scope changes after failures.
Root cause:
The deterministic shell classifier omitted destructive Git variants, global package-manager forms, and external mutation CLIs. The fixed reviewer contract only allowed same_strategy verdicts after Auto policy moved bounded workspace recovery to the automatic path.
Fix:
- Classify destructive Git state changes, global package operations, HTTP/GitHub API mutations, and remote command families as hard boundaries while preserving proven read-only fast paths.
- Allow reviewer continue verdicts for bounded, task-aligned strategy and scope changes while keeping risk and uncertainty fail closed.
- Align the three-failure rationale and add policy and classifier regression coverage.
Verification:
- go test ./... -count=1
- go test -race ./internal/recovery ./internal/control ./internal/agent -count=1
- go vet ./...
- golangci-lint v2.12.2 ./internal/recovery/... (0 issues)
- ./scripts/cache-guard.sh
* fix(auto): scope reusable recovery approvals safely
Problem:
Auto Guard repeatedly interrupted bounded external operations, while several unlisted shell, PowerShell, Git checkout, and attached GitHub CLI flag forms could bypass the intended hard boundary.
Root cause:
Recovery confirmations were one-shot only and the shell classifier relied on an incomplete deny list without a host-owned semantic authorization scope.
Fix:
Add task-scoped semantic grants for explicitly bounded operations, expose opt-in actions across Desktop, CLI, and Bot, fail closed for unknown mutations, and cover destructive sibling forms and lifecycle boundaries.
Verification:
go test ./...
go test -race ./internal/recovery ./internal/control ./internal/agent
pnpm test:typecheck && pnpm test
pnpm run build
./scripts/cache-guard.sh
* refactor(auto): simplify recovery confirmation UI
Problem:
- Auto Guard confirmations exposed recovery diagnostics at equal visual priority, making simple decisions noisy.
Root cause:
- Raw recovery fields reused warning cards and appeared before the user's actual choices.
Fix:
- Show one concise action summary, recommend the safer alternative first, move optional task grants after the decisions, and flatten technical diagnostics behind disclosure.
Verification:
- pnpm test
- pnpm typecheck
- pnpm check:css
- In-app browser checks for default, expanded, and 620px layouts with no console errors.
* feat(auto): add optional recovery guidance
Problem: Auto Guard offered only a generic retry or one-shot continue, so users could not steer a safer alternative without adding another heavyweight decision.
Root cause: the simplified recovery surface removed free-text feedback entirely instead of keeping it behind progressive disclosure.
Fix: add a quiet guidance entry that expands an inline editor, routes text only through revise, clears unrelated task grants, restores keyboard focus, and localizes the flow.
Verification: pnpm test; pnpm typecheck; pnpm check:css; interactive in-app browser review of collapsed and expanded states.
* fix(auto): bind task grants to exact targets
Problem:
Task-scoped recovery grants for git pushes matched every ref on the same remote, stale grants remained attached to reused task IDs, and Desktop described a checked task grant as a one-shot continue.
Root cause:
The semantic grant key omitted the destination ref, runtime state did not retire grants when the host task scope changed, and recovery approval events exposed only a boolean instead of the host-classified scope.
Fix:
Scope reusable pushes to an explicit remote and destination, reject ambiguous or behavior-changing variants, retire grants on task-scope changes, expose a transient display scope to every frontend, and group explicit consent with the Continue action.
Verification:
go test ./...
go vet ./...
go test -race ./internal/recovery ./internal/eventwire ./internal/control ./internal/agent -count=1
pnpm test:all && pnpm build
(cd desktop && go test ./... -count=1)
./scripts/cache-guard.sh
* test(auto): cover legacy task-grant events
Problem:
Older backends do not send the optional task_grant_scope field.
Root cause:
The new exact-scope rendering tests only exercised events produced by the updated backend.
Fix:
Keep one grantable recovery fixture on the legacy wire shape and assert that Desktop renders the generic bounded-scope explanation.
Verification:
pnpm test:typecheck
pnpm exec tsx src/__tests__/decision-surface.test.tsx
* fix(auto): drain recovery writes before new session
Problem:
The macOS PR job intermittently failed while TempDir removed a session directory because /new could return with an old-session recovery snapshot still writing asynchronously.
Root cause:
ClearSession drained path-bound recovery persistence before deleting artifacts, but NewSession rotated the session path without the equivalent lifecycle barrier.
Fix:
Drain pending recovery writes for the old path before the final session snapshot and path rotation. Add a deterministic blocking persistence regression test that proves NewSession waits for the drain.
Verification:
- go test ./... -count=1
- go test -race ./internal/control -run '^(TestFreshSessionRotationsClearRecoveryState|TestNewSessionWaitsForPendingRecoveryPersistence)$' -count=10
- go test ./internal/control -run '^(TestFreshSessionRotationsClearRecoveryState|TestNewSessionWaitsForPendingRecoveryPersistence)$' -count=50
- go vet ./internal/control ./internal/recovery
- (cd desktop && go test ./... -count=1)
- ./scripts/cache-guard.sh
* feat(remote): add rpcwire, RuntimeAPI registry, and protocol codegen
Problem:
Remote Workbench needs a bidirectional JSON-RPC transport and a single Go
registry that generates schema, Schema Hash, and TypeScript DTOs so Desktop and
Host negotiate strictly by schema, not by full Build ID hard-fail.
Root cause:
main-v2 lacked rpcwire and a generative Remote protocol surface.
Fix:
- Port transport-neutral NDJSON JSON-RPC (rpcwire) with strict frames, concurrent
requests, cancel, and size limits (adapted from #6725).
- Port RuntimeAPI protocol registry + schema generator; regenerate committed
artifacts for main-v2 eventwire.
- Mark eventwire large fields externalizable for content-ref offload without
changing provider-visible semantics.
- Extend directions for Provider Broker (host_to_client_request /
client_to_host_notification) and register broker methods in the frozen surface.
Verification:
- go test ./internal/rpcwire ./internal/remote/protocol ./internal/remote/protocolgen ./internal/eventwire
- go run ./cmd/remote-protocol-gen -check -root .
Co-authored-by: SivanCola <[email protected]>
Co-authored-by: taibai233 <[email protected]>
* feat(remote): add Desktop Provider Broker and TargetManager
Problem:
Workbench Hosts must consume models without holding API keys, and the main
window needs Local + one Remote adapters with a single active projection.
Root cause:
No in-process Broker over rpcwire and no target fencing manager on main-v2.
Fix:
- Desktop Broker endpoint: catalog, stream open/cancel, chunk/end notifications,
concurrency limits, DeepSeek ToolCallReasoning catalog flags, connection-wide
stream cancel on close (direction from #6722; fencing ideas from #6725).
- TargetManager: permanent Local, optional Remote, last-click-wins fencing,
busy remote host-swap rejection, restart reconnect hint only (no auto SSH).
Verification:
- go test ./internal/remote/broker ./internal/remote/workbench/target
Co-authored-by: SivanCola <[email protected]>
Co-authored-by: taibai233 <[email protected]>
* feat(remote): workbench SSH, attach-workspace, adapters, mirror, trust
Problem:
Remote Workbench needed end-to-end transport, lifecycle, main-window adapters,
workspace-safe files, mirrors, and Provider trust — not Serve HTML or HTTP
Gateway tunnels.
Root cause:
Prior foundation only had protocol/Broker/TargetManager; no SSH stdio path,
attach proxy, runtime grace, or UI wiring.
Fix:
- Windows: system OpenSSH + AskPass + Job Object (adapted from #6725); remote
command is `reasonix remote attach-workspace --stdio`.
- macOS/Linux: Go SSH session stdio to the same attach entry.
- attach-workspace + per-workspace runtime on Unix socket; 5-minute grace after
detach; Schema Hash gate on initialize.
- Desktop Workbench adapters (connect/disconnect/switch Local, remote RPC,
Provider trust prompt); OpenRemoteWorkspace uses workbench path.
- File containment (relative refs, no symlink leaves, atomic writes).
- Local mirror store with digest validation; restore requires session.jsonl.
- Durable Provider trust store (host+fingerprint → allowed refs only).
- Frontend workbenchTarget helpers + tests.
Verification:
- go test ./internal/remote/workbench/... ./internal/remote/broker ./internal/rpcwire ./internal/remote/protocol ./internal/cli
- cd desktop && go test -run 'TestRemote|TestWorkbench|TestAskPass|TestOpenRemote'
- pnpm exec tsx --test desktop/frontend/src/__tests__/workbench-target.test.ts
Co-authored-by: SivanCola <[email protected]>
Co-authored-by: taibai233 <[email protected]>
* docs(remote): record source PR authors as repository co-contributors
Problem:
Repository co-contribution attribution requires explicit documentation and
commit-level Co-authored-by trailers for the source PR authors (#6722, #6725).
Root cause:
PR body credits alone do not enter the GitHub contribution graph.
Fix:
Document both source PR authors, what was adopted, and that GitHub co-author
attribution uses Co-authored-by trailers with public noreply emails in
REMOTE_WORKBENCH docs (EN + zh-CN).
Verification:
- docs/REMOTE_WORKBENCH.md contains Repository co-contributors table
- every commit on this branch includes both Co-authored-by trailers
Co-authored-by: SivanCola <[email protected]>
Co-authored-by: taibai233 <[email protected]>
* fix(remote): close the workbench broker runtime loop
Problem:
Remote Workbench still mixed Remote state with the Local controller, left parts of the runtime API empty, and could lose reconnect, operation, history, Git, and provider semantics in real main-window use.
Root cause:
The first integration established the transport and broker direction but did not fully fence target projections, rebuild controller profiles, persist remote execution state, or cover strict protocol and process lifecycle parity.
Fix:
- route the existing main window through the Host-owned controller, files, Git, history, catalogs, operations, goals, checkpoints, and slash completion
- keep provider credentials on Desktop while forwarding strict provider requests, effort, DeepSeek tool-call reasoning, and reconnect-safe stream generations
- harden Build ID/schema negotiation, SSH/AskPass process cleanup, runtime socket ownership, workspace containment, mirror pulls, disconnect fencing, and background target isolation
- add generated protocol contracts and regression coverage for no-Host-key broker use, paging, reconnect, overflow, lifecycle, and projection behavior
Verification:
- env -u DEEPSEEK_API_KEY REASONIX_RELEASE_CACHE_GUARD=1 go test -race ./...
- cd desktop && go test ./... && go vet ./... && go build ./...
- pnpm --dir desktop/frontend test:all && pnpm --dir desktop/frontend build
- go run ./cmd/remote-protocol-gen -check -root .
- Windows amd64 cross-compilation for root Remote packages and desktop module
- scripts/check-cache-impact.sh
Co-authored-by: SivanCola <[email protected]>
Co-authored-by: taibai233 <[email protected]>
* fix(remote): bind attach to its workspace target
Problem:\nCI lint found an overwritten normalized workspace, and initialize could replace the SSH attach target before runtime startup.\n\nRoot cause:\nThe attach bootstrap normalized Options.Workspace but then assigned the client-provided initialize workspace to the same variable without comparing the two.\n\nFix:\nKeep the normalized attach target, normalize the requested workspace separately, and reject mismatches before schema negotiation or runtime startup.\n\nVerification:\n- go test ./internal/remote/workbench/attach ./internal/remote/workbench/runtime\n- go vet ./internal/remote/workbench/attach\n- golangci-lint v2.12.2: 0 issues
* fix(desktop): remove the legacy remote child window
Problem:\nThe same-window Remote Workbench still carried an unreachable child-window launcher and special shell lifecycle, causing Desktop CI unused-code failures and contradicting the selected product model.\n\nRoot cause:\nThe App bindings were migrated to TargetManager, but the old ticket-based Serve HTML launch path and its window-specific startup branches were left behind.\n\nFix:\nRemove remote-window tickets, spawning, middleware, lifecycle branches, and obsolete helpers so every SSH workbench runs in the existing main window.\n\nVerification:\n- cd desktop && go test ./...\n- desktop and root golangci-lint v2.12.2: 0 issues\n- Windows amd64 Desktop cross-compilation\n- wails generate module
* fix(remote): preserve workspace diff parity
Problem: After rebasing onto #6733, WorkspaceChangeDetail still executed against the Desktop filesystem while a Remote target was active.
Root cause: The frozen RuntimeAPI had no method for the newly added per-file workspace diff query.
Fix: Add a strict workspace/changeDetail contract, route Desktop through the Host runtime, and generate bounded Git/checkpoint patches inside the selected remote workspace.
Verification: go test ./...; go test -race ./...; cd desktop && go test ./...; pnpm test:all; protocol codegen check; vet/build/lint; Windows cross-compile.
Co-authored-by: SivanCola <[email protected]>
* Harden Remote provider broker lifecycle
Problem: the local Provider Broker became active before the live SSH peer was rebound to its trusted fingerprint, and slow provider consumers could block detach while the Host mutex was held.
Root cause: peer validation happened after client.Connect returned, while Host stream delivery performed blocking channel sends inside the global lock.
Fix: gate Broker activation on the authenticated live peer identity and move ordered Host delivery into bounded per-stream queues with a sole output pump.
Verification: go test -race ./internal/remote/broker; go test ./internal/remote/workbench/client; cd desktop && go test -race . -run 'Test(AuthorizeWorkbenchPeerFailsClosed|Workbench|Remote)' -count=1.
* Restore Remote sessions across runtime restarts
Problem: Remote sessions existed only in the runtime process map, so a normal detach followed by runtime exit made persisted transcripts invisible on reconnect.
Root cause: the Host had no durable mapping between protocol session identities, controller profiles, topics, and transcript paths.
Fix: add a per-workspace atomic session registry, lazily rebuild controllers after Broker activation, retain temporarily unavailable records, and persist lifecycle/profile changes without holding the runtime mutex across file I/O.
Verification: go test -race ./internal/remote/workbench/runtime -count=1; go test -race ./internal/remote/workbench/runtime -run 'TestRuntime(RestoresSessionRegistryAfterProcessRestart|EarlyShutdownDoesNotOverwriteUnreadRegistry)' -count=20.
* Keep Remote projections and provider state current
Problem:
Remote callbacks stayed bound to a stale local tab, provider metadata and authorization were captured at connect time, and the bounded-handler regression test released its blockers before overload dispatch was observable.
Root cause:
Local navigation was not serialized with Remote activation, Broker descriptors omitted compaction/pricing fields, provider closures captured immutable config, and the rpcwire test synchronized only handler startup.
Fix:
Fence visible local navigation and rebind callbacks on Remote activation, propagate complete non-secret provider metadata, refresh config and trust-scoped access for live Broker connections, fail closed on empty authorization, and wait for all overload responses before releasing handlers.
Verification:
go test -race ./internal/boot ./internal/remote/protocol ./internal/remote/broker ./internal/remote/workbench/client ./internal/remote/workbench/runtime ./internal/rpcwire
(cd desktop && go test -race ./...)
go test -race ./internal/rpcwire -run TestInboundHandlerConcurrencyIsBoundedWithoutBlockingResponses -count=100
go run ./cmd/remote-protocol-gen -check -root .
./scripts/cache-guard.sh
* Constrain restored Remote transcript paths
Problem:
An edited session registry could name an opaque topic that the wire contract permits but restoration rejected, while a nested transcript path could traverse a directory symlink outside SessionDir.
Root cause:
Registry validation derived a topic_ prefix from an opaque protocol ID and only performed lexical containment plus a final-component symlink check.
Fix:
Accept any non-empty opaque topic ID, require runtime transcripts to be direct children of SessionDir, close partially built controllers on restore errors, and isolate fallback socket paths per test run.
Verification:
go test -race ./internal/remote/workbench/runtime -count=3
* Fence late Remote projection callbacks
Problem:
A callback or snapshot refresh captured for one local tab could complete after Remote was rebound to another tab and still emit into the stale surface.
Root cause:
Reactivation marked Remote visible before rebinding callbacks, and callback/refresh completion trusted its captured tab ID instead of the current projection binding.
Fix:
Bind callbacks before activation, route late events through the current Remote tab under the transition fence, redirect snapshot completion to the live binding, and drop completion while Local is active.
Verification:
cd desktop && go test -race -run 'TestWorkbenchLateCallbackUsesCurrentProjectionTab|TestSetActiveTabSwitchesRemoteProjectionOnlyForDifferentTab|TestConcurrentActivateTopicSerializesSingleSurfacePruning' -count=20
* Synchronize Remote host-key prompt regression
Problem: The full desktop race suite detected an unsynchronized map write in the host-key prompt serialization test.
Root cause: The test started the first prompt goroutine before registering the second managed host, allowing the goroutine to read the manager map while the test wrote it.
Fix: Register every host before launching concurrent prompts and wait for both prompt goroutines during test cleanup.
Verification: cd desktop && go test -race -run '^TestHostKeyPromptsAreSerializedForGlobalDialog$' -count=100 .; cd desktop && go test -race ./...
* Wait for queued background task teardown
Problem: Windows CI could finish TestBackgroundTaskReturnsBeforeSlotFrees while its background job was still persisting transcript artifacts, causing TempDir cleanup to fail with a non-empty directory.
Root cause: The test waited only for provider startup and then slept for 50 milliseconds instead of synchronizing with the job's terminal lifecycle.
Fix: Scope the job to the test session, capture its returned job ID, wait for jobs.Done, and close the manager on every exit path before temporary-directory cleanup.
Verification: go test -race ./internal/agent -run '^TestBackgroundTaskReturnsBeforeSlotFrees$' -count=100; REASONIX_RELEASE_CACHE_GUARD=1 go test -race ./internal/agent; REASONIX_RELEASE_CACHE_GUARD=1 go test ./...; GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go test -c ./internal/agent; go vet ./internal/agent; golangci-lint run ./internal/agent/...
* fix(remote): fall back to server root workspace
* fix(remote): close review blockers for workbench v1
Problem: Remote Workbench could silently expose the Host root, strand a session after registry persistence failure, leave local provider streams running after cancellation, and fail the Ubuntu/Windows attach tests.
Root cause: One-click workspace resolution used an implicit root fallback; profile changes closed or mutated live controllers before durable state succeeded; Broker delivery lacked an abort path and open cancellation did not reach Desktop; attach readiness and tests depended on connection and timing assumptions that were not portable.
Fix: Require explicit workspace selection, roll profile changes back while preserving the old controller, propagate best-effort Broker cancellation with abortable delivery, remove the readiness probe generation race, and use deterministic platform-aware attach tests.
Verification: Remote, Broker, attach, runtime, rpcwire, race, Desktop, frontend Remote, TypeScript, production build, protocol generation, cache guard, go vet, and repeated cancellation/attach regressions passed.
* fix(remote): make attach startup portable
Problem:
The Windows CI attach integration test could not observe the in-process AF_UNIX listener and reported that the runtime never became ready.
Root cause:
Startup readiness was inferred with os.Stat on the socket path, but Windows AF_UNIX does not expose a portable socket file. A separate readiness connection is also unsafe because runtime connections own generations.
Fix:
Let the real attach connection retry bounded socket dials until the runtime listener accepts it, and return immediately after starting the in-process test runtime.
Verification:
- attach integration test repeated 50 times
- attach race test repeated 30 times
- Windows amd64 test binary cross-compiled
- go test ./internal/remote/...
- remote broker, attach, and runtime race tests
* fix(remote): keep runtime sockets portable
Problem:
Windows CI still could not connect to the in-process workbench runtime even with bounded dial retries.
Root cause:
The generated AF_UNIX address exceeded the platform path limit when the remote home itself was long, as on Windows hosted runners.
Fix:
Compact the runtime socket hierarchy while keeping it scoped under the Reasonix home, and add a regression test against the portable AF_UNIX length bound.
Verification:
- attach integration test repeated 50 times
- runtime socket path tests repeated 20 times
- attach and runtime race tests
- Windows amd64 runtime test binary cross-compiled
- go test ./internal/remote/...
- protocol generation check and cache guard
* fix(desktop): preserve native macOS app ownership
Problem:
Packaged macOS builds used reasonix-guard as CFBundleExecutable, so the Dock and LaunchServices owned a non-UI parent while the Wails child owned the window. The app could stay running without reopening its window from the Dock.
Root cause:
The packaging step replaced Wails' native bundle entry point to route startup through Guard. macOS application identity cannot be transferred reliably across that parent-child process boundary.
Fix:
Keep reasonix-desktop as the macOS bundle executable, retain Guard and the CLI as recovery sidecars, and run the equivalent failed-install, crash-loop rollback, repair, and Safe Mode preflight in Desktop before Wails creates WebView. Update packaging contracts and recovery documentation for the platform-specific launch model.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- go test ./... (desktop module)
- go test -race -run 'TestDesktopStartupRecovery|TestDesktopPackagesPreserveNativePlatformLaunchers' -count=1 . (desktop module)
- go vet ./... (desktop module)
- DESKTOP_BUILD_SKIP_DMG=1 scripts/desktop-build.sh darwin/arm64 v0.0.0
- Signed packaged App launched with reasonix-desktop as PID 1 child, one Desktop process, no Guard process, and an onscreen CoreGraphics window
* fix(desktop): satisfy startup recovery lint
Problem:
The Desktop CI job rejected the macOS startup recovery helper because continueLaunch was initialized before every return path overwrote it.
Root cause:
The named return value carried an unnecessary default assignment that go vet did not flag but golangci-lint's ineffassign check did.
Fix:
Initialize only the Safe Mode result and let each branch set the continue decision explicitly.
Verification:
- go test -run 'TestDesktopStartupRecovery|TestDesktopPackagesPreserveNativePlatformLaunchers' -count=1 .
- go vet ./...
* fix(remote): close workbench runtime safety gaps
Problem:
Remote Workbench could reject typed client requests before session hydration, execute duplicate mutations more than once, lose the committed target during a failed replacement, race controller lifecycle changes, and expose Local saved-session bindings while Remote was active.
Root cause:
Client authority hydration preserved serialized zero values, the advertised requestId contract had no runtime ledger or request sequencer, TargetManager stored candidates as committed state, and unsupported Remote history actions silently fell through to Local bindings.
Fix:
Authoritatively hydrate client-owned fields while preserving retry IDs, serialize runtime requests with a bounded 24-hour mutation replay ledger, fence controller profile transitions, promote replacement targets only after successful connection, and fail closed for unsupported Remote saved-session operations.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- env -u DEEPSEEK_API_KEY REASONIX_RELEASE_CACHE_GUARD=1 go test -race ./...
- cd desktop && go test ./... && go vet ./... && go build ./...
- pnpm --dir desktop/frontend test:remote
- go run ./cmd/remote-protocol-gen -check -root .
- scripts/check-cache-impact.sh with the live PR body
Co-authored-by: taibai233 <[email protected]>
* fix(remote): keep session rotations authoritative
Problem: Completed turns could retain stale approval or ask prompts, while committed new/clear session rotations could return an error after a registry write failure and leave clients on the old runtime epoch.
Root cause: Turn completion only cleared replay events when no prompt remained, session rotation treated the fallible registry write as part of the already-committed controller mutation, and the client did not apply new/clear result epochs.
Fix: Clear terminal prompt state, return and adopt the committed rotation epoch while logging registry degradation, reset stale runtime state, and invalidate the previous snapshot.
Verification: go test ./...; go test -race ./internal/remote/workbench/runtime ./internal/remote/workbench/client; cd desktop && go test ./...; go vet ./...; go run ./cmd/remote-protocol-gen -check -root .
* fix(remote): fence snapshot refreshes and fork cleanup
Problem:
Remote rewind could report failure after destructive state changes committed, late subscribe responses could restore stale runtime authority, and failed forks left transcript artifacts on disk.
Root cause:
Post-commit registry errors were treated as rollbackable, subscribe responses had no request-authority fence, and fork rollback removed only the in-memory child session.
Fix:
Keep committed rewind results authoritative, serialize and revision-fence subscription refreshes with automatic stale retry, validate projection freshness, and remove all artifacts for uncommitted forks.
Verification:
- go test ./...
- go test -race ./internal/remote/workbench/client ./internal/remote/workbench/runtime ./internal/control
- go vet ./...
- cd desktop && go test ./...
- go run ./cmd/remote-protocol-gen -check
* fix(remote): serialize projections and capability boundaries
Problem: Remote subscribe snapshots could overwrite newer events, unsupported Remote Memory surfaces could reach Local state, and the broker test assumed concurrent notification arrival order.
Root cause: Snapshot publication, event callbacks, and authority-changing results had no shared projection boundary; Desktop bindings did not fail closed for frozen capabilities; the test ignored stream sequence numbers.
Fix: Linearize subscribe commits with session delivery and mutation results, install the initial Remote projection within that boundary, reject unsupported Remote Memory and Auto Research access, and assert broker chunks by sequence.
Verification: go test ./...; go test -race ./internal/remote/...; desktop go test ./...; desktop go vet ./...; broker round-trip with coverage at count 500.
* fix(remote): preserve active workbench transport
Problem:
Uninitialized socket probes could evict the live Desktop connection, target transition events exposed candidates as active, and Remote notification overload silently dropped ordered events.
Root cause:
Connection ownership was transferred before initialize response commit, candidate identity and active identity shared one event shape, and rpcwire used best-effort concurrent notification dispatch.
Fix:
Add provisional connection gates with post-write activation and exact daemon build checks; keep transition events on the committed target and fence submits; add ordered bounded notification queues with explicit overflow failure and generation-aware Broker delivery.
Verification:
go test ./...
go test -race ./internal/rpcwire ./internal/remote/broker ./internal/remote/protocol ./internal/remote/workbench/...
(cd desktop && go test ./...)
(cd desktop/frontend && pnpm test:all)
* fix(remote): serialize connection teardown
Problem:
A client closing immediately after initialize could race the response-commit callback, leaving a rejected gate paired with newly activated connection ownership.
Root cause:
Provisional connection teardown and post-write activation checked and mutated the wire registry under separate critical sections.
Fix:
Serialize teardown with connection activation, keep the production notification queue memory bound at 256 entries, and lock the initialize post-write contract with a router regression test.
Verification:
go test ./internal/remote/protocol ./internal/remote/workbench/runtime
go test -race ./internal/remote/workbench/runtime ./internal/remote/protocol ./internal/remote/broker ./internal/rpcwire
* test(remote): cover broker-backed tool loop
Problem:
The no-Host-key integration test only proved a single text response. It did not exercise the critical model-to-Host-tool-to-model loop or DeepSeek reasoning replay across the Desktop Broker.
Root cause:
The stub Provider always returned final text on its first request, so Host tool execution and the second Broker request were outside the end-to-end coverage.
Fix:
Make the stub request read_file first, assert the Host result and reasoning/tool-call replay in the second request, then complete the turn and verify projected tool events.
Verification:
- go test ./internal/remote/workbench/runtime -run '^TestRuntimeControllerUsesDesktopBrokerWithoutHostKey$' -count=1 -v
- go test -race ./internal/remote/workbench/runtime -run '^TestRuntimeControllerUsesDesktopBrokerWithoutHostKey$' -count=20
- go test ./internal/remote/workbench/... ./internal/remote/broker ./internal/rpcwire
- env -u DEEPSEEK_API_KEY go test ./...
* test(remote): add physical Windows Linux acceptance
Context:
The Remote Workbench release gate needs reproducible evidence from a real Windows OpenSSH client to a Linux Host, including Broker-backed tool execution and reconnect cleanup.
Change:
Add an opt-in Windows ARM64-compatible acceptance test that exercises the production SSH transport, Linux workspace queries, Desktop Broker tool replay, session authority after reconnect, and OpenSSH process reaping. The fixture is environment-gated and keeps Provider execution on the Desktop side.
Verification:
- GOOS=windows GOARCH=arm64 CGO_ENABLED=0 go test -c -tags reasonix_remote_integration ./desktop
- go test ./...
- go test -race ./internal/remote/...
* fix(remote): align Desktop and Host build identity
Context:
The Desktop is built from a nested Go module, so Go does not embed vcs.revision there while the bundled Host CLI built from the root module does. Strict Remote Workbench negotiation consequently rejects matching release artifacts as different builds.
Fix:
Allow release builds to link a validated shared source revision into the protocol package. Capture the clean repository revision before packaging mutations and inject it into both the Wails Desktop and bundled CLI sidecar, preserving the dirty suffix for local builds.
Verification:
- go test ./...
- go test ./... (desktop module)
- go test -race ./internal/remote/...
- Windows ARM64 acceptance cross-compile with linked source revision
- bash -n scripts/desktop-build.sh
* fix(remote): preserve direct SSH credentials
Context:
The Windows Workbench direct-host branch reduced a saved Host to username, address, and port before starting OpenSSH. Explicit identity_file and proxy_jump settings were silently dropped, so valid direct connections could fail authentication or bypass the required jump path.
Fix:
Add an injection-safe direct configured transport that preserves identity and ProxyJump as distinct argv values. Route the Windows Workbench direct branch through it while retaining normalized IPv4 and IPv6 target parsing.
Verification:
- go test ./... (desktop module)
- go test -race focused Windows Workbench transport tests
- Windows ARM64 acceptance cross-compile
* fix(remote): share runtime build identity
Context:
The detached workspace runtime duplicated Build ID discovery instead of using the shared protocol implementation. Linked source revisions used by nested Desktop release builds were therefore ignored, causing one installed CLI to reject its own runtime as a different build.
Fix:
Use protocol.CurrentBuildID for production runtime identity while retaining the explicit SourceRevision test seam. This keeps Desktop, attach, and detached runtime negotiation on one validated source of truth.
Verification:
- go test ./internal/remote/workbench/runtime
- go test -race ./internal/remote/workbench/runtime
- linked-revision focused regression test
* fix(remote): allow initial history page
Context:
The Desktop history client intentionally requests the latest page without a cursor, and the Host runtime supports that state. The wire DTO nevertheless marked cursor as required, so strict validation rejected every initial session/history request after a successful Remote turn.
Fix:
Make the history cursor optional, regenerate the frozen protocol artifacts, and cover omission plus strict decoding. Strengthen the physical acceptance fixture to verify both product version and exact source revision before connecting.
Verification:
- go test ./...
- go test ./... (desktop module)
- go run ./cmd/remote-protocol-gen -check -root .
- Windows ARM64 acceptance cross-compile
* fix(remote): validate session mirror snapshots
Context:
After a completed turn the reconnect snapshot includes the persisted session mirror. That complete object incorrectly populated originalBytes while marked non-truncated, violating the frozen externalized-field contract and turning session/subscribe into an internal error.
Fix:
Emit complete session mirrors without truncation-only metadata and cover the artifact contract directly.
Verification:
- go test ./...
- go test -race ./internal/remote/workbench/runtime
- focused session mirror and runtime integration tests
* fix(remote): model session mirror payload
Context:
Remote reconnect snapshots advertised /mirror/session.jsonl as an externalized content pointer, but SessionSnapshot had no schema field at that path. Strict response validation therefore rejected every post-turn reconnect snapshot before Desktop could fetch the mirror.
Fix:
Add the mirror placeholder to the frozen snapshot DTO, replace its concrete body with null during marshaling, and keep the body behind the existing bounded contentRef transfer. Regenerate Go/TypeScript protocol artifacts and prove that no session body is inlined on the wire.
Verification:
- go test ./...
- go test ./... (desktop module)
- go run ./cmd/remote-protocol-gen -check -root .
- Windows ARM64 acceptance cross-compile
* test(remote): add live Desktop Broker acceptance
* test(remote): require live Broker tool loop
---------
Co-authored-by: taibai233 <[email protected]>
* fix(mcp): trust installed readers in subagents
Problem:
User-installed MCP tools that declared readOnlyHint could be exposed as readers but then fail before dispatch in planners and read-only subagents. Users were told to configure a second per-tool reader policy after already installing or authorizing the server.
Root cause:
Strict reader trust did not treat explicit install or an exact project launch grant as authority for non-destructive server metadata. Direct, lazy, and on-demand paths also duplicated cached-to-live safety reconciliation, while trusted_read_only_tools remained wired through configuration and Desktop contracts.
Fix:
Authorize non-destructive readOnlyHint tools for installed or launch-authorized servers, carry reader intent only for trusted readers, and share one cached-to-live safety reconciliation boundary across lazy and on-demand adapters. Remove trusted_read_only_tools from runtime, Desktop, examples, and docs while accepting and dropping legacy input on the next save.
Verification:
- env -u DEEPSEEK_API_KEY go test -count=1 ./...
- cd desktop && go test -count=1 ./...
- cd desktop/frontend && pnpm test:typecheck
- cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- ./scripts/cache-guard.sh
- ./scripts/check-cache-impact.sh
- git diff --check
* refactor(mcp): simplify installed server authorization
Problem:
User-installed MCP servers still depended on official-catalog and per-tool reader trust layers, so direct, lazy, and on-demand paths could disagree and strict child sessions could reject an already authorized reader.
Root cause:
Server authorization, signed package status, cached reader trust, and legacy plan-mode allowlists were modeled as separate overlapping decisions.
Fix:
Remove the official MCP catalog, signature/revocation plumbing, package verification, per-tool cached trust, and obsolete reader settings. Use one server-level authorization source for user installs, host-session installs, and exact project grants while retaining live read-only, destructive, and capability-fingerprint checks.
Verification:
- go test ./internal/plugin ./internal/agent ./internal/boot ./internal/config ./internal/installsource ./internal/pluginpkg ./internal/mcplaunch ./internal/planmode
- go test ./internal/acp ./internal/agent ./internal/plugin ./internal/boot
- cd desktop && go test ./...
- ./scripts/cache-guard.sh
- git diff --check
* refactor(mcp): make install authorization final
Problem:
Installed MCP servers could still encounter server, per-tool, writer, or destructive approval branches, and users had to understand settings that duplicated the install decision.
Root cause:
MCP authorization, cached reader trust, approval policy, and process sandboxing evolved as separate paths across direct, lazy, and on-demand execution.
Fix:
Treat install or exact project identity confirmation as the server authorization boundary, keep only explicit global deny plus Plan/read-only and sandbox safety checks, remove catalog and approval configuration, and unify the persistent MCP process sandbox.
Verification:
env -u DEEPSEEK_API_KEY go test ./...
go build ./...
cd desktop && go test ./...
cd desktop/frontend && pnpm typecheck
cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
cd desktop/frontend && pnpm exec tsx src/__tests__/decision-surface.test.tsx
* chore(mcp): remove retired cache helper
Problem:
CI lint found writeBoolKV left unused after per-tool reader trust was removed.
Root cause:
The cached reader-map fingerprint was deleted, but its serialization helper remained.
Fix:
Remove the dead helper.
Verification:
go test ./internal/plugin
go vet ./internal/plugin
go build ./...
* refactor(mcp): stop blocking schema-only drift
Problem:
Authorized MCP calls could be rejected when a cached tool schema differed from the live server even though authorization and safety classification were unchanged.
Root cause:
A capability fingerprint mixed schema compatibility with read-only and destructive execution safety across direct, lazy, and on-demand paths.
Fix:
Remove the tool-level schema fingerprint chain, keep live server authorization and read-only/destructive reclassification checks, and refresh changed schemas for the next session without interrupting the current authorized call.
Verification:
- go test ./internal/plugin ./internal/agent ./internal/tool
- env -u DEEPSEEK_API_KEY go test ./...
- go vet ./...
- go build ./...
- ./scripts/cache-guard.sh
- pnpm --dir site test
- cd desktop && go test ./...
* fix(mcp): preserve short Windows temp paths
Problem:
Windows stdio MCP servers inherited a deeply nested Reasonix private TEMP path. Servers such as MATLAB create watchdog Unix sockets below TEMP and exit before initialization when the path exceeds the platform limit.
Root cause:
MCP private-state preparation redirected TMP, TEMP, and TMPDIR below the workspace-scoped mcp-state directory on every platform even though Reasonix does not currently provide an OS sandbox on Windows.
Fix:
Keep the host temporary-directory environment for Windows stdio MCP processes while retaining private cache and state directories. Preserve the existing private temporary directory behavior on other platforms and add focused regression coverage for both branches.
Verification:
- go test ./internal/plugin -count=1
- GOOS=windows GOARCH=amd64 go test -c ./internal/plugin
- go test ./...
- go vet ./internal/plugin
- git diff --check
* Simplify MCP runtime authorization state
Use Spec.Authorized as the sole resolved server authorization result so installed and host-session MCP servers no longer depend on launch-manager presence.
Keep exact identity grants for project sources, align planner and strict read-only registries with non-destructive readOnlyHint metadata, and document the cooperative trust boundary.
Add source and agent-surface matrix coverage while preserving the main provider-visible MCP tool surface.
* Limit MCP identity digests to project servers
Problem: User-installed and host-session MCP servers still resolved and hashed their executable identity during every startup even though installation was already the authorization decision.
Root cause: Project launch authorization and schema-cache ownership shared generic fingerprint terminology, and start() computed the project identity before checking whether launch approval was required.
Fix: Skip project identity resolution for installed servers, remove dead and non-participating identity state, and rename the remaining concepts to ProjectLaunchIdentityDigest and SchemaCacheKey while preserving existing JSON keys and digest bytes.
Verification: go test ./internal/mcplaunch ./internal/plugin ./internal/capability ./internal/boot ./internal/control ./internal/agent; go test ./...; cd desktop && go test .; go vet ./...; ./scripts/cache-guard.sh; git diff --check.
* Restore MCP 0.53 compatibility surfaces
Problem: MCP authorization was simplified, but the current client still lacked four compatibility surfaces present in the 0.53 line: legacy SSE, workspace roots, progress notifications, and registry browsing/install.
Root cause: the Go transport abstraction dropped inbound notifications and rejected legacy SSE; runtime specs did not carry a separate workspace root; the registry UI and CLI had no official MCP Registry client.
Fix: add inbound request/progress routing across transports, implement legacy SSE with same-origin endpoint protection, advertise roots/list, and add an explicit cached official Registry marketplace to CLI and desktop. Remove stale approval documentation and the dead helper that broke lint.
Verification: go test ./...; focused go test -race for plugin/registry/CLI/control; go vet in root and desktop; desktop go test ./...; frontend production build and marketplace UI test; Windows amd64 cross-build; official Registry browse smoke test.
* Avoid MCP progress capacity overflow
* Align MCP authorization documentation
Problem: CLI help and Chinese documentation still contained ambiguous or obsolete wording that implied installed MCP writer or destructive calls could require per-call approval.
Root cause: The server-level authorization simplification removed MCP approval modes, but not every user-facing explanation was updated consistently.
Fix: State explicitly that install or exact project identity confirmation authorizes the server, that writer and destructive hints do not prompt per call, and that deny, Plan, and strict read-only filtering remain separate boundaries.
Verification: go test ./internal/cli; go vet ./internal/cli; reasonix mcp help smoke test; stale-wording scan; git diff --check.
* Harden MCP registry installs and SSE replies
Problem:
Registry cache freshness was shared across queries, cached entries could flow into install paths, and legacy SSE server requests spawned an unbounded reply goroutine per request.
Root cause:
The cache stored one global timestamp, Resolve reused Search's offline fallback, the desktop installed browse payloads directly, and the SSE reader delegated every reply to a new goroutine.
Fix:
Track freshness per Registry query with legacy-cache compatibility, require live resolution before CLI and desktop installs, and serialize SSE replies through a bounded single-worker queue with finite deadlines.
Verification:
- env -u DEEPSEEK_API_KEY go test -count=1 ./...
- env -u DEEPSEEK_API_KEY go test -race -count=1 ./internal/mcpregistry ./internal/plugin
- env -u DEEPSEEK_API_KEY go test -count=1 ./... (desktop)
- env -u DEEPSEEK_API_KEY go vet ./... (root and desktop)
- pnpm run test:all
- pnpm run build
- env -u DEEPSEEK_API_KEY ./scripts/cache-guard.sh
- git diff --check
* Reject unavailable live registry installs
Problem:
A Registry entry could become non-installable between browsing and the desktop's live re-resolution, leaving the generic add flow with an invalid empty connection. The SSE concurrency regression also used a fixed delay after routing the client response.
Root cause:
The desktop resolver verified freshness but not the resolved entry's installability, and the test inferred worker startup from elapsed time.
Fix:
Validate the live entry through PluginEntry before returning it to the installer, and synchronize the SSE test on the first reply POST instead of sleeping.
Verification:
- env -u DEEPSEEK_API_KEY go test -count=5 ./internal/plugin
- env -u DEEPSEEK_API_KEY go test -race -count=3 ./internal/plugin
- env -u DEEPSEEK_API_KEY go test -count=1 ./...
- cd desktop && env -u DEEPSEEK_API_KEY go test -count=1 ./...
- git diff --check
* feat(desktop): add line numbers, virtual scroll, and search to file preview (#6557)
Replace the default HljsCode editor with LineNumberCode, which adds: - Line-number gutter (right-aligned, selectable via CSS user-select) - Virtual scrolling via @tanstack/react-virtual for files > 100 lines - Ctrl+F search bar with Codex-style controls: N of M counter, prev/next arrows, case-sensitive (Aa) and whole-word (ab) toggles - Inline <mark> highlighting of matched text within syntax-highlighted lines - Row dimming for non-matching lines during search
Applies to both the workspace file preview (WorkspacePanel) and chat tool output (ToolCard read_file / read_session), where the existing %*d arrow prefix is stripped before rendering to avoid double line numbers.
Also raise filePreviewLimit from 256 KB to 2 MB so the workspace panel can show complete files.
* Fix file preview search regressions
Problem:
The file preview viewer intercepted find globally, corrupted escaped-code searches, dropped copy controls, and lost multiline lexer state.
Root cause:
The line-number viewer became the default CodeViewer, reused a stateful regex across lines, and searched serialized highlight.js HTML.
Fix:
Scope the enhanced viewer to workspace previews, preserve original tool output, map raw match offsets onto balanced highlighted rows, restore copy controls, and keep the shortcut local to the focused viewer.
Verification:
- npm test
- npm run build
- Browser verification of workspace line numbers, scoped find, and copy layout
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* Document workspace preview limits
Problem:
The desktop editor documentation still described obsolete PlainCode seams, and localized truncation notices still reported the previous 256 KB preview limit.
Root cause:
The file preview implementation and backend limit changed without updating the developer documentation or user-facing locale strings.
Fix:
Document the opt-in searchable line-number viewer and 2 MiB preview behavior, update all supported locale notices, and add a contract test that keeps the backend limit and translations aligned.
Verification:
- tsx src/__tests__/workspace-preview-css.test.ts (21 passed)
- tsc --noEmit
- npm run check:css
- npm run build
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* Credit #6570 workspace preview contribution
Context:
PR #6570 independently established the workspace-only opt-in line-number approach that is retained and extended in PR #6574.
Attribution:
Credit the adopted scope boundary, adaptive gutter design, and non-selectable line-number behavior to the source PR contributor.
Verification:
- Compared both PRs against the latest main-v2
- Confirmed PR #6574 preserves the adopted workspace-only rendering boundary
- git diff --check
Co-authored-by: youuoypi <[email protected]>
Co-authored-by: SivanCola <[email protected]>
* fix(desktop): harden file preview search
Problem:
The file preview search skipped the first result, rebuilt the entire highlighted document on each query, and exposed untranslated or incomplete controls.
Root cause:
Search navigation advanced before the pending query committed, while matching and markup generation were coupled across the full source.
Fix:
Debounce and cap matching, inject marks only into rendered lines, use a plain-text fallback for large files, reveal the first result, and localize and label the search controls.
Verification:
- npm test
- npm run build
- go test ./...
- in-app browser search regression
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
Co-authored-by: youuoypi <[email protected]>
Coverage gap:
The merged topic archive guard lacked direct regressions for active detached runtimes and the turn-admission race window.
Tests:
- verify a running detached runtime keeps its controller, binding, session, and topic metadata when archive is rejected
- force SubmitToTab to hold runtime admission while TrashTopic waits, then verify the active-work recheck rejects the archive atomically
Verification:
- go test . -run '^(TestTrashTopicRejectsRunningDetachedRuntime|TestTrashTopicWaitsForConcurrentTurnAdmission)$' -count=50
- go test -race . -run '^(TestTrashTopicRejectsRunningDetachedRuntime|TestTrashTopicWaitsForConcurrentTurnAdmission)$' -count=1
- go test ./...
- git diff --check
* fix(desktop): unify updater state and network fallback
Problem: The startup banner and Settings used independent updater state, while manifest and signature requests could fail permanently on a broken IPv6 route.
Root cause: Each useUpdater caller owned a separate state machine, and only artifact downloads had an IPv4 fallback. A stalled metadata request could also consume the whole retry window.
Fix: Lift updater state into one provider, give manifest transports bounded attempt budgets, retry transient metadata failures over IPv4, and avoid retrying permanent HTTP statuses.
Verification: pnpm test:all; pnpm build; go test ./...; go vet ./...
Co-authored-by: HaoyueQin <[email protected]>
* fix(desktop): harden platform update handoff
Problem: macOS could replace the app before the old process fully exited, and Windows surfaced an opaque fork/exec failure when the copied helper was blocked or invalid.
Root cause: The macOS handoff used a fixed one-second delay, while the Windows path copied and started the helper once without validating its PE image, architecture, or copied bytes.
Fix: Wait for the exact macOS PID, log and roll back failed relaunches, verify the Windows helper before handoff, retry transient security locks, and return actionable path-safe errors.
Verification: go test ./...; go vet ./...; Windows ARM64 tests and the stamped asInvoker helper executed successfully in a Parallels Windows 11 VM.
Co-authored-by: ttmouse <[email protected]>
---------
Co-authored-by: HaoyueQin <[email protected]>
Co-authored-by: ttmouse <[email protected]>
* fix(desktop): show Windows update progress
Problem:
Windows auto-update closed Reasonix and ran NSIS silently, making a healthy handoff look like the installer never started.
Root cause:
The update helper always passed /S and hid the NSIS child window, so users had no feedback after the desktop exited.
Fix:
Add a dedicated /REASONIXUPDATE mode that keeps the verified in-place directory fixed, skips welcome and directory pages, displays localized progress, auto-closes on success, and preserves helper and Guard recovery. Update UI copy and lock the handoff contract with regression tests.
Verification:
- cd desktop && go test ./...
- cd desktop && go vet ./...
- cd desktop/frontend && pnpm typecheck
- Windows amd64/arm64 desktop and helper cross-compilation
- makensis 3.12 installer compilation with three language tables
* fix(desktop): close Windows updater after install
Problem:
Windows auto-updates showed the visible installer progress but stopped on the Finish page, so the helper could not relaunch Reasonix without user input.
Root cause:
The update mode enabled automatic close on the install-files page, but the unconditional MUI Finish page still remained in the page sequence.
Fix:
Skip the Finish page only for /REASONIXUPDATE=1 while preserving the normal manual installer wizard, and add a packaging contract test for the page hook.
Verification:
- cd desktop && go test ./...
- Built the Windows ARM64 NSIS installer
- Ran the cached update helper in Parallels Windows 11 and verified visible progress, automatic installer exit, helper exit code 0, automatic relaunch, and reasonix v0.0.0-pr6751
Problem:
- The first-run gate only checked for a DeepSeek key, so configured custom and no-auth local providers still saw an irrelevant setup modal.
- Users could not durably defer setup, and the alternative-provider action opened model usage instead of provider access.
Root cause:
- Onboarding eligibility was tied to one credential rather than the effective provider configuration.
- The frontend gate had no persisted dismissal state or explicit model-access target.
Fix:
- Accept any enabled, configured provider with chat models and restore DeepSeek access after successful quick setup.
- Add a versioned local dismissal marker, a persistent setup banner, and direct routing to Settings > Models > Access.
- Refresh the localized onboarding copy and cover backend and frontend contracts.
Verification:
- cd desktop && go test ./...
- cd desktop/frontend && pnpm exec tsx src/__tests__/startup-settings-contract.test.ts
- cd desktop/frontend && pnpm build
- Verified both onboarding and banner routes in the in-app browser.
Problem:
The README's third-party Star History image stopped rendering after GitHub restricted access to timestamped stargazer history.
Root cause:
The unauthenticated chart service can no longer read this repository's detailed stargazer data, so GitHub Camo receives an upstream failure.
Fix:
Generate accessible light and dark SVG charts in a trusted secret-backed workflow, publish them to a dedicated asset branch, and reference those repository-hosted images from both READMEs.
Verification:
- chart generator syntax and 5 unit tests passed
- actionlint and git diff checks passed
- full repository CI passed on Linux, macOS, and Windows
- race, CodeQL, desktop, site, and security checks passed
* docs: add VS Code extension install links
Summary:
- document the Reasonix VS Code extension in both READMEs
- add Marketplace and Open VSX install paths to the website
- cover the new deep link and responsive download layout
Verification:
- cd site && npm test
- cd site && npx astro build
- Chromium checks at 1280px and 320px
* docs: remove obsolete rewrite banner
Context:
The top-level 1.0 rewrite notice describes main-v2 as a new default and duplicates migration guidance that remains linked in the documentation section.
Change:
Remove the transitional notice from both English and Chinese READMEs while preserving the dedicated migration guide links.
Verification:
- git diff --check
- confirmed both README migration links remain
* docs: clarify Reasonix install paths
Context:
The bilingual README grouped install channels without helping readers choose between CLI, Desktop, editor, and source workflows.
Change:
Reorganize installation guidance into explicit paths with a platform table and first-run steps. Keep project support details at the bottom in a collapsed section so they remain available without distracting from the documentation.
Verification:
- git diff --check
- verified release, desktop, extension, source, and donation links
* docs: simplify support footer
Context:
The collapsed support block added unnecessary interaction at the very end of the README.
Change:
Replace the disclosure control in both README variants with a subtle horizontal divider and compact centered label while keeping the support details visible.
Verification:
- git diff --check
- inspected the bilingual footer markup
* docs: separate CLI and desktop quick starts
Context:
The README quick-start mixed CLI commands with desktop usage and duplicated detailed configuration guidance already covered by the docs.
Change:
Keep a minimal CLI/TUI command flow, add a distinct desktop download-and-launch path, and replace the inline configuration example with links to the authoritative CLI and configuration guides.
Verification:
- git diff --check
- verified bilingual documentation targets
- verified the desktop download URL returns HTTP 200
* docs: streamline documentation links
Context:
The README documentation section presented a long flat list, and its embedded Star History image currently fails to load.
Change:
Group bilingual documentation links into three concise categories, surface the configuration-path guide, label English-only links in the Chinese README, and replace the broken chart embed with a stable page link.
Verification:
- git diff --check
- verified every referenced documentation file exists
- verified the Star History page returns HTTP 200
* docs: restore the Star History chart
Context:
The README should display the existing project trend chart by default instead of reducing it to a text link.
Change:
Restore the exact bilingual Star History picture markup currently used by the upstream online README, including light and dark theme sources.
Verification:
- git diff --check
- compared both restored sections byte-for-byte with origin/main-v2
* docs: keep README links language-specific
Context:
The Chinese README documentation index still linked to three English-only pages, making the language routing inconsistent.
Change:
Remove English-only Spec, migration, and checkpoint links from the Chinese index and retain them in the English README. Rename the remaining Chinese engineering group to match its contents.
Verification:
- git diff --check
- confirmed every Chinese index link targets a zh-CN document
- confirmed the English index contains no zh-CN links
* docs: add missing Chinese references
Context:\nThe Chinese README should keep the same documentation coverage as the English README without sending Chinese readers to English-only pages.\n\nChange:\nAdd Chinese engineering spec, migration, and checkpoints references, and restore their links in README.zh-CN.md.\n\nVerification:\nValidated Markdown fence balance, relative links, privacy-sensitive strings, and git diff whitespace.
* site: place VS Code install tab last
Context:
The VS Code install channel should follow the existing desktop and package-manager options in the homepage tab order.
Change:
Move the VS Code tab after Homebrew and add a source contract assertion for the channel order.
Verification:
Ran the complete site test suite and verified the deep link still selects the VS Code pane in the local browser preview.
* site: clarify editor extension install tab
Context:
The install pane covers both Visual Studio Marketplace and Open VSX, so a VS Code-only label is too narrow.
Change:
Rename the tab to Editor extension / 编辑器扩展 while preserving the vscode deep-link key, and add a bilingual copy contract assertion.
Verification:
Ran all site tests and verified the local deep link selects the renamed pane.
* site: streamline hero secondary links
Context:
The hero secondary install guidance was a dense sentence with weak scanability and low visual contrast.
Change:
Turn it into a concise bilingual “More ways to use Reasonix” navigation row with wrapping, clearer hierarchy, visible link affordances, and focus styling.
Verification:
Ran all site tests and verified the local homepage in the in-app browser.
* docs: clarify web dashboard recommendation
Context:
The migration guide said v2 no longer had a web dashboard, contradicting the supported `reasonix serve` browser UI.
Change:
Keep the web dashboard documented while recommending the Wails desktop app as the primary visual experience in both languages.
Verification:
- cd site && npm test
- git diff --check
- confirmed the obsolete no-dashboard wording is absent
* docs: clarify explicit Goal activation
Problem:
The Chinese engineering spec did not explicitly state that AutoResearch requires an explicitly started Goal.
Root cause:
The translated summary predated main-v2's rule that ordinary chat never changes collaboration mode or creates durable AutoResearch state.
Fix:
State that users must choose Goal or run /goal before long-horizon work can enable AutoResearch, and that ordinary chat stays ordinary.
Verification:
- cd site && npm test
- git diff --check
- compared the Chinese wording with the latest main-v2 Goal contract
* docs: clarify explicit Plan activation
Problem:
The Chinese engineering spec did not state that Plan Mode requires an explicit user choice.
Root cause:
The translation predated main-v2 retiring automatic Plan Mode.
Fix:
Document that ordinary chat cannot enter Plan automatically, that tool approval posture is independent, and that an approved-plan execution window cannot approve later plans.
Verification:
- cd site && npm test
- git diff --check
- compared the Chinese wording with the latest main-v2 Plan contract
Add reviewed bilingual product notes for v1.17.17 and correct the affected product surfaces.
Verification:
- release-notes validation and rendering
- full pull request CI across Linux, macOS, and Windows
- public-content privacy scan
* fix(controller): clear stale running/finishing/approval state on close / 清除关闭 Controller 时残留的运行/完成/审批状态
* Fix controller close cancellation lifecycle
Problem:
Controller.Close could leave a foreground turn or approval waiter alive, while the original PR cleared status bookkeeping independently of the goroutine lifecycle.
Root cause:
Close sealed admission but did not cancel the active foreground context. A late completion could also re-enter the finishing state after teardown.
Fix:
Cancel the captured foreground context after sealing admission, clear prompts only alongside that cancellation, keep running truthful until the goroutine exits, and prevent closed completions from reopening the finishing gate. Add focused regressions for pending asks and late TurnDone delivery.
Verification:
- go test ./internal/control
- go test -race ./internal/control
- go test ./...
- focused desktop runtime lifecycle tests
- git diff --check
---------
Co-authored-by: SivanCola <[email protected]>
* fix(desktop): prevent model list option overlap when provider has many models
Removes the undersized min-height (32px) from .provider-model-draft__option
that was smaller than the actual minimum content height (~46px), causing
option cards to visually overlap when model names wrapped to multiple lines.
Adds overflow: hidden to prevent content bleeding, grid-auto-rows: min-content
to size each grid row to its tallest card, and bottom padding to prevent the
last row from touching the container edge.
Closes#5563, #5585, #5785, #6480
* test(desktop): guard provider model grid sizing
Context: PR #6539 fixes provider model card overlap by correcting implicit grid row sizing and removing the undersized card minimum.
Coverage: Assert that model rows stay content-sized, option cards keep no explicit minimum height, and unexpected overflow remains contained.
Verification: typography-overflow-contract.test.ts passed with 127 tests; CSS syntax and z-index checks passed; git diff --check passed.
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* fix: retire automatic Plan Mode
Problem:
Automatic Plan Mode created two different planning experiences, and users who had enabled it kept silently entering Plan Mode after the setting was removed.
Root cause:
The preference was persisted across TOML, desktop, CLI, controller, and classifier paths without a retirement migration or a single explicit-mode contract.
Fix:
Remove the setting and automatic classifier/runtime paths, migrate pre-v5 configs to manual-only behavior, retain safe read compatibility for old TOML and Wails clients, and update docs and metrics.
Verification:
- go test ./internal/config ./internal/control ./internal/boot ./internal/cli
- cd desktop && go test ./...
- pnpm build and pnpm test:typecheck in desktop/frontend
- settings-refresh-snapshot.test.tsx: 43 passed
- ./scripts/cache-guard.sh
- real-browser settings inspection with a clean console
* fix: remove obsolete desktop test helper
Problem:
The Linux desktop CI job failed golangci-lint after the Automatic Plan tests were removed.
Root cause:
A test-only turn runner and its context import were left behind without any remaining callers.
Fix:
Remove the obsolete helper and unused import.
Verification:
- cd desktop && go vet ./...
- cd desktop && go test ./...
* fix: preserve retired auto-plan compatibility
Problem:
Released desktop clients still submit the settings_auto_plan metric, and existing shell automation may still query or disable automatic Plan Mode.
Root cause:
The retirement removed both the worker enum value and the shell command, causing legacy metric batches to fail validation and safe compatibility commands to exit with an error.
Fix:
Keep settings_auto_plan as an input-only legacy metric signal, and retain a hidden config auto-plan shim that reports off, accepts off as a no-op, and rejects enable attempts. Add focused regressions for both contracts.
Verification:
- env -u DEEPSEEK_API_KEY go test ./internal/config ./internal/cli
- npm test in workers/crash-report
- npm run typecheck in workers/crash-report
- ./scripts/cache-guard.sh
- git diff --check
* fix: ignore unknown crash-report metrics
Problem:
A retired or future metric signal could invalidate an otherwise useful metrics batch, while a dedicated legacy allowlist only handled one historical signal.
Root cause:
The worker validated every counter signal with a strict enum before processing the batch.
Fix:
Keep strict validation for known counters, discard bounded unknown signals before storage, and treat an all-unknown batch as a successful no-op.
Verification:
- npm test (workers/crash-report)
- npm run typecheck (workers/crash-report)
- go test ./internal/cli -run TestConfigAutoPlanCompatibilityCommand|TestConfigLoadIgnoresRetiredAutoPlan -count=1
Problem:
Selecting a changed file opened only its commit history, so users could not inspect staged, unstaged, untracked, or session-only edits inside Reasonix.
Root cause:
The Changes view reused the git history/detail APIs and checkpoints exposed metadata without an earliest pre-edit file state. There was no tab-scoped current-change contract for the frontend.
Fix:
Add a read-only current-change API that prefers the Git HEAD-to-worktree patch and falls back to the earliest session checkpoint. Render semantic line-numbered diffs with change totals, keep commit history secondary and collapsed, and reject stale same-tab session responses.
Verification:
- go test ./internal/checkpoint ./internal/control
- cd desktop && go test ./...
- pnpm build
- pnpm test:typecheck
- pnpm exec tsx src/__tests__/workspace-changes-errors.test.tsx (30 passed)
- Real browser verification at 429 px with no console warnings or errors
* fix(goal): require explicit Goal start for AutoResearch
Problem:
Ordinary prompts with strong long-horizon wording could silently switch the session into Goal + AutoResearch, create durable task state, and change the collaboration mode without an explicit user choice.
Root cause:
The controller default submit path and the TUI send path both invoked a dedicated ordinary-prompt AutoResearch classifier before routing a normal turn.
Fix:
Remove the implicit promotion paths, classifier, and interface while preserving AutoResearch selection inside explicitly started Goals, explicit --research/--simple flags, and existing task recovery. Update controller and TUI regressions plus user-facing documentation.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- cd desktop && go test ./...
- go test ./internal/cli -run TestStrongResearchPromptStaysInOrdinaryMode -count=1
- ./scripts/cache-guard.sh
- site tests and Astro static build
* test(goal): exercise task-path prompts through a real turn
Problem:
The AutoResearch task-path regression test constructed a controller without a runner, so it would also pass against the removed implicit auto-start implementation.
Root cause:
The legacy controller only mutated Goal state after detecting a task-path prompt when a runner was present, while the test asserted only the runner-less state.
Fix:
Wire the test through a scripted provider and real agent turn, wait for completion, and assert the original task-path prompt reaches the provider without Goal or AutoResearch injection.
Verification:
- env -u DEEPSEEK_API_KEY go test ./internal/control ./internal/cli
- env -u DEEPSEEK_API_KEY go test ./...
- git diff --check
* fix(ccswitch): import MCP servers with enabled_reasonix
CC Switch DB v16 tracks Reasonix MCP enablement in enabled_reasonix /
apps.reasonix. Prefer those columns when importing into Reasonix plugins,
with a transitional fallback to enabled_codex / apps.codex so older rows
still import until users flip the Reasonix toggle.
* fix(ccswitch): make Reasonix enablement backward-safe
Problem:
- The new enabled_reasonix query failed on legacy CC Switch databases and treated Codex enablement as active even when Reasonix was explicitly disabled.
Root cause:
- The importer referenced enabled_reasonix unconditionally and could not distinguish an absent legacy JSON flag from an explicit false value.
Fix:
- Detect the SQLite schema before choosing the Reasonix or legacy Codex filter, and preserve JSON field presence so explicit Reasonix enablement is authoritative.
Verification:
- go test -v ./internal/config -run Test\(CCSwitch\|LoadCCSwitch\)
- go test -race ./internal/config -run Test\(CCSwitch\|LoadCCSwitch\)
- go test ./internal/config ./internal/cli ./internal/control
- go vet ./internal/config ./internal/cli ./internal/control
- ./scripts/cache-guard.sh
* docs(ccswitch): clarify Reasonix fallback semantics
Problem:
- The importer comment and PR description implied that v16 rows could still fall back to Codex enablement.
Root cause:
- The documentation was not updated after fallback changed from a row-level OR to schema and field-presence detection.
Fix:
- Describe Reasonix enablement as authoritative when present and limit Codex fallback to pre-v16 SQLite schemas or legacy JSON without the Reasonix field.
Verification:
- go test -count=1 ./internal/config -run 'Test(CCSwitch|LoadCCSwitch)'\n- git diff --check
---------
Co-authored-by: SivanCola <[email protected]>
* fix(desktop): isolate WebView2 startup from stale proxies
Problem:
On Windows, Reasonix can remain invisible when a proxy client exits without clearing an unreachable system proxy. WebView2 stalls before DOM-ready while the Wails window is still StartHidden.
Root cause:
The WebView2 browser process inherits the Windows system proxy even though the desktop shell loads bundled assets through wails.localhost and remote workspaces through loopback URLs. There was also no visible timeout path when DOM-ready never arrived.
Fix:
Start the vendored WebView2 Chromium with --no-proxy-server for the embedded and loopback shell, while leaving Reasonix provider, updater, MCP, and SSH traffic on the separate Go proxy configuration. Add a 15-second Windows watchdog that shows the native window and a bilingual recovery prompt when startup remains incomplete.
Verification:
- cd desktop && go test ./...
- cd desktop && go vet ./...
- cd desktop && wails generate module
- Windows amd64 and arm64 desktop test binaries cross-compiled successfully
- Windows amd64 vendored WebView2 edge tests cross-compiled successfully
- ./scripts/cache-guard.sh
* fix(desktop): preserve proxying for remote images
Problem:
Isolating the Windows WebView2 shell from the system proxy would also make remote Markdown and Mermaid image resources bypass Reasonix proxy settings. The startup watchdog could additionally fire in remote-window test contexts that do not carry a Wails frontend context.
Root cause:
Remote image URLs were still loaded by the WebView network stack, while the watchdog was enabled for every Windows WebView process instead of only the primary desktop window.
Fix:
Route remote Markdown and Mermaid image resources through a bounded, SSRF-guarded backend endpoint that uses the current Reasonix proxy specification, validates raster content, and sanitizes SVG. Scope the Windows startup fallback to the primary window only.
Verification:
- cd desktop && go test ./...
- cd desktop && go vet ./...
- cd desktop && go test -race . -run 'RemoteMarkdownImage|WindowsWebView2StartupFallback|AwaitStartupFallback|RemoteWindowLifecycle' -count=1
- cd desktop/frontend && pnpm test
- cd desktop/frontend && pnpm typecheck
- cd desktop/frontend && pnpm build
- Windows amd64 and arm64 desktop test binaries cross-compiled successfully
- ./scripts/cache-guard.sh
* chore(security): document guarded image fetch
Problem:
CodeQL cannot infer that the remote Markdown image request is constrained by the custom URL, redirect, DNS, and dial-time guards, so it reports the intentional fetch sink as an uncontrolled critical request.
Root cause:
The security boundary is implemented across helper functions and a proxy-aware transport rather than a sanitizer recognized by the standard CodeQL data-flow model.
Fix:
Add the repository-standard scoped CodeQL annotation at the single guarded request sink, with the direct-dial and trusted-proxy DNS boundary documented inline.
Verification:
- cd desktop && go test . -run RemoteMarkdownImage -count=1
- cd desktop && go vet .
* chore(security): format CodeQL suppression
Problem:
The scoped CodeQL suppression was not recognized because its explanatory text shared the directive line.
Root cause:
CodeQL requires the codeql[query-id] directive to occupy its own comment line immediately before the guarded alert location.
Fix:
Keep the security-boundary explanation adjacent to the request and place the exact suppression directive on a separate line.
Verification:
- git diff --check
- gofmt desktop/remote_markdown_image.go
* chore(security): add legacy CodeQL suppression
Problem:
The modern source suppression directive remains visible to the audit but is not honored by the PR request-forgery query post-processing.
Root cause:
This CodeQL Go query configuration still requires the legacy lgtm query-scoped suppression marker.
Fix:
Add the exact legacy marker beside the modern directive at the single guarded request sink.
Verification:
- git diff --check
- gofmt desktop/remote_markdown_image.go
* chore(security): keep image guard rationale explicit
Problem:
Neither supported source suppression marker is honored by this repository's PR security summary for the intentional remote-image fetch.
Root cause:
The query does not model the custom proxy-aware SSRF transport and retains the alert independently of inline post-processing markers.
Fix:
Remove the ineffective directives while preserving the adjacent security-boundary explanation. The alert will be handled through the repository's auditable code-scanning triage record.
Verification:
- git diff --check
- gofmt desktop/remote_markdown_image.go
* fix(desktop): pin proxied markdown image targets
Problem:
Remote Markdown images skipped destination validation whenever a proxy was selected, valid SVGs with common prologs were rejected, and per-image transports could retain idle connections.
Root cause:
The image fetcher treated proxy-side DNS as a trust boundary, evaluated dynamic proxy routing more than once, relied on an SVG prefix heuristic, and created keep-alive transports that were never reused.
Fix:
Resolve every initial and redirected image target to public unicast addresses, select the proxy route once, and dial only vetted IPs through direct, HTTP(S) CONNECT, or SOCKS paths while preserving Host and TLS SNI. Make each transport single-use and close it with the response. Parse SVG roots after safely removing BOM, comments, and directives.
Verification:
- cd desktop && go test ./...
- cd desktop && go test -race . -run 'RemoteMarkdownImage|WindowsWebView2StartupFallback|AwaitStartupFallback|RemoteWindowLifecycle' -count=1
- cd desktop && go vet ./...
- frontend Markdown/Mermaid tests and pnpm typecheck
- ./scripts/cache-guard.sh
- Windows amd64/arm64 desktop and vendored WebView2 test binaries cross-compiled
* test(remote): stabilize encrypted identity fallback
Problem: The full race job intermittently timed out while testing fallback from a stored passphrase to a per-identity prompt.
Root cause: The integration test allowed only 10 seconds for an SSH handshake that performs three CPU-bound passphrase KDF operations, which is too tight under full race-package contention.
Fix: Give this single KDF-heavy integration scenario a 30-second context while leaving production timeouts and behavior unchanged.
Verification: GOMAXPROCS=2 go test -race ./internal/remote -run '^TestClientFallsBackFromStoredPassphraseToPerIdentityPrompt$' -count=10; go test ./internal/remote -count=3; go vet ./internal/remote.
* fix(remote): align SSH config and credential handling
Problem:
Remote SSH imports could freeze resolved fields, public-key authentication could stop before a configured identity was attempted, password-only hosts had no desktop prompt, and host removal did not provide a reliable confirmation flow.
Root cause:
Reasonix parsed only a subset of OpenSSH configuration, stored resolved values instead of the alias, represented multiple public-key sources as static methods, and lacked a one-shot desktop credential channel.
Fix:
Resolve effective host settings through ssh -G while preserving aliases and legacy entries, try ordered identity files through the SSH auth callback, add secure stored and one-shot desktop credentials, and make host removal explicit and testable.
Verification:
- go test ./internal/remote ./internal/cli
- cd desktop && go test ./...
- pnpm --dir desktop/frontend test:remote
- pnpm --dir desktop/frontend build
- git diff --check
- real OpenSSH and Reasonix connections succeeded against the same configured alias
* fix(remote): complete OpenSSH parity and credential cleanup
Problem:
Remote SSH still rejected valid Match exec configs, could redirect legacy hosts through a colliding display label, ignored IdentitiesOnly, reused one passphrase across encrypted keys, left CLI-managed credentials behind, and had Windows-only test failures.
Root cause:
The embedded parser gated the system OpenSSH resolver, host names doubled as alias metadata, auth cached passphrases at host scope, credential transactions lived only in the desktop package, and tests assumed Unix home paths.
Fix:
Make ssh -G authoritative while retaining parser fallback, use the persisted Host lookup key, honor IdentitiesOnly, cache and prompt passphrases per identity, share recoverable credential mutations across desktop and CLI, and make SSH discovery assertions platform-safe.
Verification:
Passed root and desktop Go suites, remote race tests, frontend typecheck/build and focused UI tests, real Match exec resolution, multi-key encrypted auth, and Windows root/desktop cross-compilation.
* fix(remote): complete OpenSSH parity edge cases
Problem:
Remote SSH still diverged from OpenSSH for system config loading, IdentityFile none, IdentitiesOnly agent keys, error handling, and repeated imports. Sequential credential prompts could also cross-resolve in the desktop UI.
Root cause:
Alias discovery eagerly resolved every host, effective-config failures fell back too broadly, agent auth treated IdentitiesOnly as a total agent disable, and prompt identity relied only on reusable metadata. Import replacement also rebuilt entries without Reasonix-specific settings.
Fix:
Resolve selected aliases lazily through the normal ssh -G config stack, propagate installed OpenSSH failures, preserve none semantics, filter agent signers by configured public identities, add opaque prompt IDs, and retain workspace/bootstrap settings on re-import.
Verification:
Passed full root and desktop tests and vet, remote race tests, frontend remote/type/build checks, cache/security checks, real ssh -G coverage, and Windows amd64/arm64 compile validation.
* feat(acp): support mid-turn steering
Expose the existing agent steer queue through a Reasonix ACP v1 session/steer extension. Accept guidance only while a prompt is active, preserve existing frontend fallback behavior, and replay delivered steers as the user's raw text.
Refs #3470
* fix(acp): namespace the steer extension
Problem:
The ACP v1 steer extension used the reserved core method name session/steer and was not discoverable through initialize capabilities.
Root cause:
The initial adapter registered the feature like a core ACP method instead of using ACP vendor extension naming and the capability _meta contract.
Fix:
Register _reasonix.io/session/steer, advertise it under agentCapabilities._meta["reasonix.io"], and lock the wire contract with tests.
Verification:
- go test ./internal/acp ./internal/control ./internal/agent
- go test -race ./internal/acp ./internal/control ./internal/agent
- go vet ./...
- env -u DEEPSEEK_API_KEY go test ./...
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* docs(acp): add editor integration guides
Documentation: Consolidate ACP v1 startup, capabilities, session lifecycle, controls, MCP transport, permissions, and mid-turn steering into dedicated English and Chinese guides.
Website: Surface ACP from both READMEs, the main guides, the bilingual docs page, and the homepage; keep deep links visible below the fixed header after language initialization.
Verification: npm test; npm run build; go test ./internal/acp ./internal/cli ./internal/control ./internal/agent; env -u DEEPSEEK_API_KEY go test ./...; go vet ./...; git diff --check; desktop and 390px Chrome DevTools checks in English and Chinese.
---------
Co-authored-by: SivanCola <[email protected]>
Problem:
Workspace file rows could overlap and become unclickable after expanding or collapsing a directory while direct DOM positioning was enabled.
Root cause:
React keyed rows by file path, but TanStack Virtual defaulted to index keys. Inserting child rows shifted indices while the virtualizer cache still pointed at DOM elements reused by path.
Fix:
Provide the virtualizer with the existing stable workspace path key and add a runtime regression that verifies unique positions across expand, collapse, and re-expand transitions.
Verification:
- pnpm --dir desktop/frontend exec tsx src/__tests__/workspace-changes-errors.test.tsx
- pnpm --dir desktop/frontend test
- pnpm --dir desktop/frontend build
- Browser rapid-toggle check confirmed unique row positions without overlap
Release notes:
- add reviewed bilingual product notes for v1.17.16
- correct the affected product surfaces and remove two upstream issue-number collisions
- document Remote SSH platform limits and the one-time fleet schema cache cold start
Verification:
- release catalog validation and rendering tests
- site tests and production build
- citation range and privacy scans
* Fix plugin skill MCP tool bindings
Problem:
Plugin skills can reference raw MCP names or Claude-qualified names that are absent from Reasonix's provider-visible tool registry, causing otherwise valid tool calls to fail.
Root cause:
MCP registration canonicalized names, while skill imports preserved upstream instructions and allowed-tools metadata without a runtime binding layer.
Fix:
Add package-aware MCP capability bindings, resolve portable aliases only when unique, inject authoritative bindings when a plugin skill is invoked, canonicalize allowed-tools safely, and keep permission, hook, plan-mode, and audit decisions on the canonical name. Preserve provider schemas and use valid cached metadata for on-demand servers.
Verification:
- go test ./...
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check
* Harden runtime MCP skill bindings
Problem:
The initial binding layer narrowed wildcard allowed-tools entries, trusted an authored Markdown heading as an idempotence marker, and could mix stale boot-cache entries with a live server surface.
Root cause:
Pattern translation replaced rather than augmented existing patterns, preparation state was inferred from untrusted content, and cached bindings were appended without checking the session registry's authoritative servers.
Fix:
Preserve wildcard allowlists while supplementing only unmatched canonical names, track preparation with host-only Skill state, and skip cached bindings whenever that server already has registry bindings. Add focused regressions for all three contracts.
Verification:
- go test ./internal/skill ./internal/boot ./internal/tool ./internal/agent ./internal/plugin ./internal/control
- go test -race ./internal/skill ./internal/boot ./internal/tool ./internal/agent ./internal/plugin ./internal/control
- go test ./...
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check
* Prepare MCP bindings for all subagent entries
Problem:
Structured multi-skill subagent invocations and the headless subagent profile API could pass plugin skills to their runners without runtime MCP bindings.
Root cause:
Those two host entry points bypassed the preparation calls already used by run_skill, slash rendering, and dedicated wrappers.
Fix:
Prepare each structured subagent immediately before dispatch and prepare headless profiles before selecting their runner. Add focused controller regressions for both paths.
Verification:
- go test ./internal/skill ./internal/boot ./internal/tool ./internal/agent ./internal/plugin ./internal/control
- go test -race ./internal/skill ./internal/boot ./internal/tool ./internal/agent ./internal/plugin ./internal/control
- go test ./...
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check
* Scope on-demand MCP proxies in subagents
Problem:
A plugin subagent with an MCP-specific allowed-tools entry could lose that tool when the session exposed it only through use_capability. Granting the ordinary proxy would instead widen the skill to every capability.
Root cause:
Prepared allowlists carried only canonical direct tool names, and subagent registry filtering had no capability-specific proxy boundary.
Fix:
Carry stable mcp-tool capability IDs as host-only allowlist entries and synthesize a restricted use_capability wrapper only when no direct MCP binding exists. Enforce the ID boundary in both resolver and direct execution paths, including read-only subagents.
Verification:
- go test ./internal/skill ./internal/boot ./internal/tool ./internal/agent ./internal/plugin ./internal/control
- go test -race ./internal/skill ./internal/boot ./internal/tool ./internal/agent ./internal/plugin ./internal/control
- go test ./...
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check
* fix: preserve interrupted turn recovery context
Problem:
Stopping a turn or hitting a provider error discarded visible reasoning and tool progress, while the next turn could not safely determine which work had completed.
Root cause:
Cancellation cleanup removed the whole assistant/tool tail, and partial stream buffers existed only in frontend memory. Replaying those buffers directly would violate provider reasoning and tool-pairing contracts.
Fix:
Persist provider-excluded interrupted display records, retain only complete canonical tool pairs, and append a bounded structured recovery block to the next real user turn. Keep TUI and Desktop replay aligned, protect compaction and pruning paths, and add a legacy-safe orphan-tool sentinel.
Verification:
- go test ./...
- cd desktop && go test ./...
- go test -race ./internal/provider ./internal/agent ./internal/control -count=1
- go vet ./internal/provider ./internal/agent ./internal/control ./internal/cli
- pnpm exec tsx src/__tests__/use-controller-meta.test.ts
- pnpm typecheck
- ./scripts/cache-guard.sh
* fix: stabilize interrupted turns across compaction
Problem:
In-turn compaction could invalidate the recorded message index, duplicate a visible user request during recovery, and lose display-only interrupted output during explicit range summaries.
Root cause:
Recovery treated a mutable array index as a stable turn boundary, compaction could fold the active turn, and SummarizeFrom/SummarizeUpTo replaced LocalOnly records with their summaries.
Fix:
Relocate interrupted turns by their persisted start time or exact graceful fallback, preserve marker time across recovery branches, exclude the active turn from folds, and reattach LocalOnly records outside provider and archive inputs.
Verification:
go test ./...
go test -race ./internal/provider ./internal/agent ./internal/control -count=1
(cd desktop && go test ./...)
pnpm --dir desktop/frontend test:all
./scripts/cache-guard.sh
git diff --check
* fix: remove obsolete interrupted-turn wrapper
Problem:
The compaction-stability follow-up left an unreferenced one-argument cancellation wrapper, which failed the repository's golangci-lint unused check.
Fix:
Remove the dead wrapper and keep its recovery contract documented on the fallback-aware implementation used by the controller and orchestrator paths.
Verification:
go run github.com/golangci/golangci-lint/v2/cmd/[email protected].2 run --timeout=5m
go test ./internal/control
go vet ./internal/...
git diff --check
* fix(desktop): 点击快捷键录制按钮时显式聚焦,修复 WebKit 下按键无响应
桌面应用通过 WKWebView(Safari 内核)渲染,而 WebKit 点击 <button> 不会赋予其
键盘焦点;快捷键录制器的 keydown 监听恰好挂在按钮元素上,导致正式版中进入录制
状态后按任何组合键都毫无反应(开发时用 Chromium 内核浏览器测试掩盖了此问题)。
修复方式:进入录制状态时显式调用 focus(),并在按钮失焦时退出录制状态,避免界
面显示"按下组合键"实际却收不到按键。
* feat(desktop): 支持自定义对话框发送与换行快捷键
在快捷键注册表中新增 composer.send(默认 Enter)与 composer.newline(默认
Shift+Enter),均可在 设置 → 快捷键 中重新绑定。输入框的 Enter 判定改为:匹配
发送键则发送,匹配换行键则换行(默认 Shift+Enter 走浏览器原生插入,其余组合键
手动插入 "\n"),两者都不匹配的组合键不做任何事。未绑定任何动作的普通 Enter
仍插入换行,因此把发送键改为 Ctrl+Enter 后即为微信/钉钉式的操作习惯。发送按钮
悬浮提示与运行中的占位符文案通过 useShortcutComboLabel 实时显示当前绑定的组合
键,不再写死 "Enter"。
Closes#6343
* Fix composer shortcut recording boundaries
Problem:
The shortcut recorder accepted non-Enter keys that the composer runtime could not execute, and manual newlines after inline invocation badges lost their caret boundary.
Root cause:
Composer actions had no recorder-level key constraint, while newline insertion dropped the selection's afterInvocationId anchor.
Fix:
Restrict send and newline recording to Enter chords with localized validation, and preserve invocation ordering through a shared newline helper.
Verification:
- pnpm test
- pnpm run test:typecheck
- pnpm run typecheck
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* Fix composer shortcut compatibility and recorder exit
Problem: Configurable composer shortcuts made legacy modified-Enter send chords stop working under the default layout. The Enter-only recorder also consumed Tab and Escape, leaving keyboard users in recording mode.
Root cause: Unmatched modified Enter events were swallowed without preserving the previous default behavior, and recorder events were prevented before allowed-key validation and cancellation handling.
Fix: Preserve non-Shift Enter sending until the user explicitly customizes the send chord. Let Escape cancel recording and let Tab follow native focus traversal with a safe blur fallback. Add focused compatibility and keyboard-navigation regressions.
Verification: pnpm run test:all; pnpm run build; local browser keyboard interaction for Escape and Tab.
---------
Co-authored-by: SivanCola <[email protected]>
* fix(remote): handle additional host key algorithms
Problem:
Reasonix treated any key different from an existing known_hosts entry as a host-key replacement, even when the server presented a separate valid key algorithm.
Root cause:
The host-key callback only checked whether knownhosts.KeyError contained records; it did not distinguish same-algorithm key changes from a newly observed algorithm.
Fix:
Keep same-algorithm key changes fail-closed, but route keys from new algorithms through the existing fingerprint confirmation and managed known_hosts persistence flow. Add regression coverage for both paths.
Verification:
- go test ./internal/remote/... -count=1
- go vet ./internal/remote/...
- go test ./...
- go vet ./...
* fix(remote): prefer recorded host key algorithms
Problem:
Multi-key SSH servers could negotiate a host-key algorithm different from the one recorded in known_hosts, while accepting that different key through TOFU would weaken host identity verification.
Root cause:
The client used the Go SSH default host-key order instead of preferring algorithms already trusted for the destination.
Fix:
Restore fail-closed mismatch handling and derive a secure host-key preference order from matching known_hosts records before each target or ProxyJump handshake. Add real multi-key negotiation and mismatch regressions.
Verification:
- go test ./internal/remote/... -count=1
- go vet ./internal/remote/...
- go test -race ./internal/remote -run HostKey -count=1
- go test ./...
- go vet ./...
* fix(remote): preserve host key compatibility
Problem:
The host-key preference list removed legacy algorithms required by already trusted RSA-only hosts and treated certificate-authority keys as ordinary host identities.
Root cause:
Algorithm ordering was rebuilt only from secure algorithms, and knownhosts.KeyError.Want does not expose whether a matching record is marked @cert-authority.
Fix:
Preserve Go SSH legacy fallback algorithms behind secure choices, promote them only with matching recorded key formats, and inspect matched records with ssh.ParseKnownHosts so marker entries do not influence ordinary host-key preference. Add real reconnect and host-certificate regressions.
Verification:
- go test ./internal/remote/... -count=1
- go vet ./internal/remote/...
- go test -race ./internal/remote -run "TestNewSSHClient|TestHostKeyPolicy" -count=1
- go test ./...
- go vet ./...
- git diff --check
* Fix MCP handshake and trust recovery
Problem:
- Stdio MCP servers could block initialization when they sent a request among startup notifications.
- Workspace re-verification could launch a second copy of a single-instance server, while first-time project approval was mislabeled as re-verification.
Root cause:
- The stdio read loop treated every inbound method message as a notification and never answered server requests.
- Desktop trust preflight ran before disconnecting the shared live client.
Fix:
- Distinguish notifications from server requests, answer ping and unsupported methods, serialize writes, and preserve the final message before EOF.
- Stop the live server before exact trust preflight, restore or reconnect shared registries safely, and label first-time approval as Authorize and connect.
Verification:
- go test ./...
- cd desktop && go test ./...
- go test -race ./internal/plugin
- cd desktop && go test -race . -run TestSetMCPTrustWorkspaceRefreshesEverySharedHostRegistry -count=1
- cd desktop/frontend && pnpm test && pnpm typecheck && pnpm test:typecheck
- go vet ./...
- GOOS=windows GOARCH=amd64 go test -c -o /dev/null ./internal/plugin
- ./scripts/cache-guard.sh
- ./scripts/check-cache-impact.sh
- git diff --check
* Reply to MCP server requests off the read loop
Problem: readLoop answered server-initiated requests by writing to the
shared stdin pipe under writeMu. With both pipe buffers full, readLoop
could wait on writeMu held by a client call blocked on the jammed stdin,
while the server blocked writing the stdout that only readLoop drains — a
four-way deadlock. A failed reply write also killed the whole transport
even when the read side was still healthy.
Root cause: the only goroutine draining stdout was allowed to block on
the stdin write path.
Fix: route server-request replies through a bounded queue owned by a
dedicated replyLoop goroutine. readLoop enqueues without blocking and
drops replies on overflow, degrading to the pre-existing
unanswered-request behavior; reply write failures stop further replies
but are no longer terminal, since pipe errors keep surfacing through the
next client call's own write.
Verification: go test ./internal/plugin -race -count=1; focused
TestStdioReadLoopStaysLiveWhenReplyWriterIsBlocked and
TestStdioInitializeHandlesNotificationsAndServerPing -count=5 -race;
go vet ./internal/plugin; GOOS=windows GOARCH=amd64 go test -c
./internal/plugin; gofmt clean.
* Serialize MCP lifecycle mutations with trust preflight
Problem: SetMCPTrust snapshots each tab's enabled state and holds
runtimeRebuildMu through the trust preflight, but SetMCPServerEnabled,
ReconnectMCPServer, and the other MCP mutation entry points did not take
that lock. A disable issued while a workspace re-verification preflight
was in flight returned immediately, and the trust reconnect then
re-registered the server's tools from the stale snapshot — exposing
tools the user had just disabled and desynchronizing the
provider-visible registry.
Root cause: the trust preflight and the MCP enable/reconnect/edit entry
points mutated the same Host/Registry state under different locks.
Fix: route every Wails-exposed MCP mutation (inspect/set trust, add,
update, remove, reconnect, clear auth, enable/disable, tier, catalog
revocation apply) through lockMCPMutation, which serializes on
runtimeRebuildMu with the documented lock order runtimeRebuildMu ->
App.mu -> Host/Registry. Also refresh the stale transport interface
comment: server notifications are ignored, ping is answered, and
unsupported server requests get JSON-RPC method-not-found.
Verification: desktop go test ./... passed; focused
TestSetMCPTrustSerializesConcurrentDisable and
TestSetMCPTrustRestoresConnectionAfterPreflightFailure with -race
-count=3 passed; go build ./...; go vet; gofmt clean.
* Hold the MCP lifecycle lock across plugin uninstall
Problem: RemovePlugin disconnected the uninstalled plugin's MCP servers
through its OnDisconnect callback before rebuild() acquired
runtimeRebuildMu. Racing a workspace trust preflight, the unlocked
disconnect could interleave so the trust reconnect relaunched the
just-removed server from its stale snapshot, leaving an orphaned process
attached to the shared Host.
Root cause: the uninstall flow was the one remaining Wails MCP mutation
entry point outside lockMCPMutation.
Fix: RemovePlugin now holds the MCP lifecycle lock across the uninstall
and calls rebuildSettingLocked("plugins") so the rebuild does not
re-acquire runtimeRebuildMu. A desktop-wide sweep confirmed no other
live-MCP mutation site remains outside the lock.
Verification: TestRemovePluginSerializesWithTrustPreflight fails on the
unlocked implementation and passes with the lock (mutation-tested); the
four MCP lifecycle regressions pass with -race; desktop go test ./...
passed; go build ./...; go vet; gofmt clean.
* Make plugin uninstall atomic and clean every runtime
Problem: RemovePlugin checked for active runtime work before waiting on
the MCP lifecycle lock, so work starting during a lock wait (up to the
30s trust preflight) was only detected by the rebuild after the plugin
was already deleted — the user got a failure for an uninstall that had
happened. Its OnDisconnect callback also only disconnected the active
tab's controller: sibling tabs on the shared Host kept provider-visible
tools backed by the closed client, and other workspaces kept running the
uninstalled server.
Root cause: the pre-lock active-work check was trusted across the lock
wait, and the uninstall treated a global plugin as active-tab state.
Fix: RemovePlugin re-checks active work under the lifecycle lock and the
active tab's turn gate, holds that gate through the uninstall and
rebuild (rebuildSettingTurnLocked is the turn-gate-free split of
rebuildSettingLocked), and fails before deleting anything when work
started mid-wait. OnDisconnect now sweeps every visible and detached
runtime tab: DisconnectMCPServer stops the shared client once per Host
and drops the stale tool prefix from every other controller's registry.
Verification: TestRemovePluginRechecksActiveWorkUnderLock and
TestRemovePluginDisconnectsEveryRuntime both fail against the previous
implementation and pass with the fix (mutation-tested); the full
TestSetMCPTrust*/TestRemovePlugin* set passes with -race; desktop
go test ./... passed; go build ./...; go vet; gofmt clean.
* Gate every affected runtime during MCP trust and uninstall
Problem: RemovePlugin gated only the active tab's turn gate while
disconnecting every visible and detached runtime, so a background job on
a sibling tab let the uninstall complete and strip the running work of
its plugin MCP. SetMCPTrust checked that sharing controllers were idle
without holding their turn gates, so a turn starting between the check
and UnregisterMCPServerTools/DisconnectMCPServer lost its MCP tools
mid-flight.
Root cause: idle checks were done lock-free or under a single tab's gate
while the guarded mutation spanned every runtime sharing the state.
Fix: lockRuntimeTurnGates snapshots the affected visible and detached
tabs under runtimeRebuildMu, locks each turnStartMu in stable tab-ID
order, re-verifies active work under the gates (reading tab.Ctrl under
App.mu so detached runtimes count), and releases in reverse order.
RemovePlugin gates all runtimes across the uninstall and rebuild;
SetMCPTrust gates every tab sharing the Host across preflight, restore,
and reconnect. No path acquires turnStartMu before runtimeRebuildMu, so
the multi-gate order is deadlock-free.
Verification: TestRemovePluginRejectsBusySiblingRuntime,
TestRemovePluginRejectsBusyDetachedRuntime, and
TestSetMCPTrustWaitsForSiblingTurnGate all fail against the previous
implementation and pass with the gates (mutation-tested); the full
TestSetMCPTrust*/TestRemovePlugin* set passes with -race -count=3;
desktop go test ./... passed; go build ./...; go vet; gofmt clean.
* Freeze runtime work admission during MCP lifecycle mutations
Problem: lockRuntimeTurnGates assumed the runtime set was stable under
runtimeRebuildMu, but OpenTopicTab inserts tabs under App.mu only and
startTabControllerBuild attaches controllers from an unguarded goroutine.
A runtime appearing after the gate snapshot could start work while a
plugin uninstall was mid-flight, and during the 30s trust preflight a
newly built tab could attach to the same shared Host — relaunching a
single-instance server or leaving a registry the reverify never saw.
Root cause: the gates froze existing runtimes but nothing stopped new
work admission, so snapshot-then-gate had an unclosable enumeration race.
Fix: add runtimeAdmissionMu, a work-admission RW barrier. Turn admission
(beginTabTurn through finishTabTurnStart) and the async controller build
(buildTabControllerWithContext, covering the shared-Host attach and swap)
hold the read side; lockRuntimeTurnGates takes the write side before its
snapshot, making work admission — not tab-set stability — the invariant:
tabs created mid-hold stay inert until release. SetMCPTrust's affected
predicate now evaluates Host membership inside the gated snapshot and the
controllers list is computed after the gates, so controllers attached
just before the barrier are included. Writers acquire the barrier only
while holding runtimeRebuildMu (mutually exclusive), read holders never
acquire runtimeRebuildMu, and the order runtimeRebuildMu ->
runtimeAdmissionMu -> turnStartMu -> App.mu is one-directional, so the
barrier introduces no deadlock.
Verification: TestRemovePluginBlocksLateTurnAdmission (real beginTabTurn
path) and TestSetMCPTrustBlocksLateControllerBuild (real
startTabControllerBuild path) both fail with the barrier acquisitions
removed and pass with them (mutation-tested); the full
TestSetMCPTrust*/TestRemovePlugin* set passes with -race -count=3;
desktop go test ./... passed; go build ./...; go vet; gofmt clean.
* Harden MCP runtime lifecycle admission
Problem:
Concurrent turn admission, workspace repair, runtime teardown, and shared-host MCP edits could leak admission locks, deadlock behind a queued writer, close a captured Host mid-operation, drift to a newly active workspace, or leave sibling registries stale.
Root cause:
Admission ownership was split across callers, workspace repair recursively acquired the RWMutex read side, teardown unlinked some runtimes before entering the lifecycle barrier, MCP config helpers re-read active-tab state, and reconnect paths refreshed only one controller registry.
Fix:
Use an idempotent turn-admission token and admission-held build cores, extend the lifecycle barrier across unlink and teardown transactions, carry the invocation workspace root through config edits, and refresh every enabled controller sharing an MCP Host while preserving per-tab disabled state.
Verification:
go test ./...
cd desktop && go test ./...
go test -race ./internal/config ./internal/plugin
cd desktop && go test -race ./...
go vet ./...
cd desktop && go vet ./...
GOOS=windows GOARCH=amd64 go test -exec=true ./internal/plugin -run ^$
cd desktop && GOOS=windows GOARCH=amd64 go test -exec=true ./... -run ^$
./scripts/cache-guard.sh
git diff --check
* Fix desktop lifecycle lint findings
Problem:
The Desktop lint job rejected five dead admission wrappers left after the lifecycle refactor and an empty lock section in the queued-writer regression.
Root cause:
All production call sites had moved to admission-held cores, while the test used lock acquisition itself as an implicit signal that staticcheck could not observe.
Fix:
Remove the unused wrappers and publish an explicit channel signal while the writer owns runtimeAdmissionMu.
Verification:
cd desktop && go test -race -run ^TestBeginTabTurnWorkspaceRepairDoesNotRecursivelyLockAdmission$ -count=5
cd desktop && go test ./...
cd desktop && go vet ./...
git diff --check
* Stabilize MCP trust recovery and runtime teardown
Problem:
- Project MCP launch approval could invalidate itself after the first restart.
- Reconnect and trust recovery could select a shadowed user entry instead of the effective project entry.
- Provider teardown and shutdown could race runtime/MCP lifecycle work and leak shared-host references.
Root cause:
- Reasonix-owned offline launcher flags were included in the approved server identity.
- Lifecycle reloads used the edit-source lookup instead of merged runtime precedence.
- Several teardown paths bypassed the runtime admission barrier and did not release tab host ownership.
Fix:
- Separate canonical launcher identity arguments from enforced process arguments for npx, bunx, and uvx.
- Reload effective merged MCP entries for reconnect, toggle, and trust restoration.
- Serialize provider teardown and shutdown with runtime admission, gate live turns, and release affected shared hosts.
- Add deterministic regression coverage for authorization stability, source shadowing, lifecycle ordering, and host cleanup.
Verification:
- go test -race ./...
- cd desktop && go test -race ./...
- go vet ./... in both Go modules
- frontend tests, test typecheck, and production build
- cache guard
- Windows and Linux cross-compilation of affected packages
* Close MCP trust and runtime lifecycle gaps
Problem:
- Runtime teardown could close a controller while a foreground turn was still entering it.
- A stored uvx --from launcher lock split the option from its package value.
- Revoking project MCP trust left the authorized process alive and hid the recovery action.
Root cause:
- Ordinary teardown used the shared admission side, launcher enforcement inserted at the locator value, and connected revocation only updated trust receipts.
Fix:
- Serialize teardown with the exclusive runtime mutation barrier, keep uvx --from pairs adjacent, and disconnect every shared registry while preserving an actionable launch-approval status after revocation.
Verification:
- go test -race ./... (root and desktop modules)
- go vet ./... (root and desktop modules)
- pnpm test:all && pnpm build
- ./scripts/cache-guard.sh
- make cross and Windows desktop test compilation
* Synchronize job migration notice test
Problem:
- Linux race CI found concurrent writes to a test-owned notice slice while exercising artifact migration failure reporting.
Root cause:
- The manager can emit migration and completion notices from different goroutines, but the FuncSink test collector appended and read the slice without synchronization.
Fix:
- Guard notice collection with a mutex and inspect a synchronized snapshot.
Verification:
- go test -race ./internal/jobs -run ^TestSetActiveSessionPathReportsMigrationFailure$ -count=100
- go test -race ./internal/jobs -count=10
- go test -race ./...
- go vet ./...
* Bound bot config concurrency regression
Problem:
- Windows Go 1.26.5 CI repeatedly crashed with found pointer to free object while this lock-discipline test ran, including on main-v2 and unrelated pull requests.
Root cause:
- The test used a stop-driven background writer whose total work depended on reader and runner timing. A prior 1 ms throttle reduced but did not eliminate the unbounded allocation and GC pressure.
Fix:
- Start fixed reader and writer loops together, yield between iterations, and wait explicitly for the bounded writer to finish. The loops remain unordered so race detection still covers missing map locks.
Verification:
- focused normal test count=500
- focused race test count=200
- internal/bot normal count=20
- internal/bot race count=5
- go test -race ./...
- go vet ./...
* fix(mcp): simplify install and authorization flow
Problem:
Installing an MCP server could still lead to redundant launch and tool trust prompts, while the desktop surfaced low-level trust, isolation, and catalog maintenance controls during normal setup.
Root cause:
Project-scoped install_source entries lost their live provenance, durable launch grants did not imply ordinary-call approval, and cached placeholders were built before the established grant was applied.
Fix:
Preserve install provenance, record and roll back durable project launch grants, apply exact matching grants to live and cached tools, and reduce the desktop and CLI setup flow to add-and-connect with one confirmation only for repository-discovered servers.
Compatibility:
No persisted schema or config fields changed. Existing receipts and legacy trust, untrust, verify, and catalog commands remain supported, and MCP tool names, schemas, and registration order stay unchanged.
Verification:
- go test ./...
- pnpm test:all
- pnpm build
- git diff --check
* refactor(mcp): remove legacy trust workflow
Problem:
MCP installation and use still exposed session trust receipts, capability snapshots, re-verification, revocation, and catalog maintenance steps that interrupted normal development workflows.
Root cause:
Launch authorization and tool-call safety were coupled to a large trust state machine, so user-configured servers and repository-discovered servers shared unnecessary security UI, CLI commands, event payloads, and persisted authority.
Fix:
Replace the trust state machine with exact project launch grants, remove legacy Desktop and CLI trust surfaces, keep add-and-connect direct for user-configured servers, preserve old raw state for cross-version compatibility, and retain destructive approval, sandbox, signed revocation, and explicit read-only boundaries.
Verification:
- go test ./... and go vet ./... in root and desktop modules
- pnpm test:all and pnpm build
- focused race tests for MCP launch, plugin, agent, authorization, reconnect, and update paths
- cache guard and Windows cross-compilation
- browser verification of the simplified MCP settings flow
- git diff --check
* fix(mcp): allow incomplete drafts to return to form
Problem:
Switching an untouched or structurally valid incomplete MCP JSON draft back to the form editor was blocked.
Root cause:
Mode switching reused submission-level validation, which requires a non-empty command or URL even for the generated empty template.
Fix:
Allow incomplete connection fields only while converting JSON drafts back to the form, preserve untouched form state, and keep final submission validation strict. Add regression coverage for empty and edited incomplete drafts.
Verification:
- pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- pnpm typecheck
- pnpm test:typecheck
- Manual in-app browser mode-switch verification
* fix(mcp): honor install authorization for destructive tools
Problem:
MCP servers explicitly installed or launch-authorized by the user still prompted again whenever a tool declared destructiveHint, preventing the intended add-and-use experience.
Root cause:
The MCP permission gate evaluated destructive metadata before the effective approve policy, so the source-aware authorization default could never apply to those calls.
Fix:
Keep explicit deny as the highest priority, then let an effective approve policy run directly before destructive review. Preserve fresh review for destructive calls under auto, prompt, and writes; update lazy safety messages, tests, and public documentation to match the policy.
Verification:
- go test ./...
- go test ./internal/permission ./internal/control ./internal/plugin ./internal/agent
- pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- pnpm typecheck
- pnpm test:typecheck
- git diff --check
* fix(mcp): remove overwritten launcher preparation
Problem:
The PR lint job failed with staticcheck SA4006 in authorizeSpecLaunch.
Root cause:
The prepared spec was initialized before an if/else where both branches assigned it again, making the initial value unused.
Fix:
Declare the prepared spec without the redundant initial assignment while preserving both launch preparation paths.
Verification:
- go test ./internal/plugin
- go vet ./internal/plugin
- git diff --check
* test(mcp): make launch checks portable on Windows
Problem: Windows CI failed on POSIX-only permission semantics and could not remove a hard-linked running test executable.
Root cause: the tests assumed Unix mode bits and reused the active test binary as an MCP launcher fixture.
Fix: limit the permission-bit assertion to non-Windows hosts and use an inert executable fixture that is never held open by the test process.
Verification: go test ./internal/mcplaunch ./internal/plugin; go test ./...; GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go test -c for both affected packages.
* fix(desktop): improve Remote SSH error recovery
Problem:
- Raw SSH connection errors appeared in a global toast that could stretch across the macOS title bar and exposed machine-local diagnostics in primary chrome.
Root cause:
- The connect-and-open path forwarded backend error strings directly to the global toast, while host-key mismatch details were only available as unstructured text.
Fix:
- Preserve structured host-key mismatch details across the remote kernel and Wails bridge.
- Route connection failures to the window-level Remote SSH status popover with localized summaries and recovery actions.
- Add an explicit security details dialog and constrain generic toasts below the macOS title bar.
Verification:
- go test ./... && go vet ./...
- cd desktop && go test ./... && go vet ./...
- pnpm --dir desktop/frontend test
- pnpm --dir desktop/frontend test:remote
- pnpm --dir desktop/frontend build
* fix(desktop): recover remote connection edge states
Problem:
- Remote connection waits could time out without a visible recovery action, and degraded connections were presented as failed.
Root cause:
- The frontend could not distinguish waiter-only timeouts from backend terminal errors, and any status carrying an error was promoted to connection failure.
Fix:
- Add a typed timeout with a localized stop-and-retry action.
- Treat only stopped errors as terminal failures and render degraded SSH sessions as connected warnings.
- Apply safe localized summaries across the status bar, remote panel, and host manager.
Verification:
- cd desktop && go test ./... -count=1 && go vet ./...
- pnpm --dir desktop/frontend test:all
- pnpm --dir desktop/frontend build
- git diff --check
Problem:
Custom providers whose names begin with a digit generated an api_key_env such as 9ROUTER_API_KEY. Credential validation rejected that name, so model discovery and manual provider saves both failed.
Root cause:
The desktop and CLI provider-name normalizers allowed a numeric first character even though Reasonix credential keys require an alphabetic or underscore prefix.
Fix:
Prefix digit-leading generated stems with CUSTOM_ in both surfaces, add regression coverage for 9router, and document the generated name in English and Chinese.
Verification:
- pnpm typecheck && pnpm test (desktop/frontend)
- go test ./internal/config ./internal/cli
Problem:
- Saving conversation width or another setting could replace an active theme pack's effective base palette with the configured restore palette.
Root cause:
- The generic settings refresh reapplied configured desktop appearance without reapplying the active theme pack.
Fix:
- Centralize configured base appearance application while preserving the current theme pack.
- Reuse the helper for direct theme-mode changes and add a regression test for refresh and restore behavior.
Verification:
- pnpm --dir desktop/frontend exec tsx src/__tests__/theme-pack.test.ts
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend check:css
- pnpm --dir desktop/frontend test
- pnpm --dir desktop/frontend build
- Browser: applied Spark Notebook, switched conversation width to Full, and confirmed the pack and Aurora effective style remained active.
* feat(serve): support --port-file/--token-file/--pid-file for supervised headless serve
Extract RunGracefulListener so callers can bind first (--addr 127.0.0.1:0),
record the real port, and keep the auth token out of argv. Groundwork for the
remote-SSH module's detached remote serve bootstrap.
* feat(config): add user-global-only [remote] section for SSH hosts
[[remote.hosts]] entries carry connection/forward/workspace settings and name
credential env vars (passphrase_env/password_env) instead of secrets, mirroring
the provider api_key_env idiom. Like [secrets], loadForRoot pins the section
back to the user-global value after the project merge — with a deep copy,
because TOML decoding mutates slice backing arrays in place and a shallow pin
would alias the project-injected hosts. Remote credential env names flow into
CredentialEnvNames so they are filtered from tool subprocess environments.
Adds RemoteStateDir/RemoteKnownHostsPath and the internal/store name helpers
for the remote-side serve state layout.
* feat(remote): SSH transport kernel — dial, auth, host keys, forwards, SFTP
New internal/remote package group provides the connection layer for the
remote module:
- client.go: supervised connection with a single-goroutine state machine,
keepalive ([email protected]), full-jitter exponential-backoff
reconnect, shared SFTP (generation-checked), and forward re-attach.
- auth.go: OpenSSH-like method order (agent, identity file, default
identities, password, keyboard-interactive); interactively-obtained
secrets are cached in-process so reconnects never re-prompt.
- knownhosts.go: system known_hosts read-only + a Reasonix-managed TOFU
file; key mismatch is a non-promptable hard error.
- dial.go: ProxyJump chains (netclient proxy applies to the first hop only).
- forward/: -L (persistent local listener across reconnects) and -R
(re-registered on each attach) with typed ErrBindBusy/ErrDuplicateForward.
- sftpfs/: list/stat/capped-read-with-text-detection/atomic-write/mkdir/
rename/remove, quarantining the new github.com/pkg/sftp dependency.
- netclient/dialer.go: StreamDialer honoring the user's proxy policy
(SOCKS5 via x/net/proxy, http/https via CONNECT).
- sshconfig.go: ~/.ssh/config import via github.com/kevinburke/ssh_config.
- sshtest/: in-process SSH+SFTP server for tests.
New deps: github.com/pkg/sftp, github.com/kevinburke/ssh_config (both pure
Go). x/* versions kept at main-v2's pins.
Cache-impact: none — no prompt/tool/provider path touched.
Cache-guard: n/a (new leaf subsystem, not in the system-prompt prefix).
* feat(remote): remote serve bootstrap over SSH
internal/remote/bootstrap ensures a detached 'reasonix serve' runs for a
(host, workspace) pair: reuse a live process (the reconnect path), else detect
the remote OS/arch (uname), locate/install/upload reasonix, write a 32-byte
token 0600 via SFTP, launch serve bound to 127.0.0.1:0 with --token-file/
--port-file (token never in argv), poll the port file, and persist ServeState
JSON. Status/Stop/Logs manage the recorded process. Every remote command
operand is POSIX single-quote-escaped (golden test covers '; rm -rf ~').
V1 targets Linux/macOS remotes; cross-platform upload without a matching local
binary is a documented limitation directing to serve_install = npm.
Cache-impact: none — new leaf subsystem, no prompt/tool/provider path.
Cache-guard: n/a.
* feat(cli): reasonix remote command group + /remote slash
Adds the remote subcommand tree (add/list/remove/import/test/connect/open/
status/forward/serve/fs), mirroring the mcp command shape. Host CRUD writes to
the user-global config only (remote hosts are pinned there); connect runs a
foreground supervisor that dials, bootstraps remote serve, forwards the serve
port with the token in a loopback URL, applies configured -L/-R forwards, and
holds the tunnel until Ctrl-C while the remote serve keeps running. Terminal
prompts handle key passphrases, passwords, and TOFU host-key confirmation;
dialing honors the user's proxy settings via netclient. The /remote slash
command shows a read-only host summary. New i18n fields added to all three
catalogs.
Cache-impact: none — CLI surface + i18n only, no prompt/tool/provider path.
Cache-guard: n/a.
* feat(desktop): remote SSH Go bindings (remote_app.go)
25 App bindings behind a narrow remoteKernel interface (fake-injected in
tests): host CRUD (user-global config), Connect/Disconnect/status,
status-driven TOFU host-key confirmation (ConfirmRemoteHostKey resumes a
parked dial), SFTP list/read/write-with-mtime-conflict/mkdir/rename/delete,
port-forward CRUD, and remote serve EnsureServer/Open/Stop/Status/Logs.
desktopRemoteManager wraps internal/remote per host; status/forward/server
transitions bridge to remote:status / remote:forwards / remote:server through
the asyncRuntimeEmitter (never blocking the kernel). The manager is created
lazily on first use and closed in App.shutdown, rejecting any pending host-key
prompt. OpenRemoteWorkspace forwards the serve port and opens the tunneled URL
via Wails BrowserOpenURL; last workspace/host persist to desktop-remote.json
(all-optional fields, backward-compatible).
desktop/go.mod gains pkg/sftp + kevinburke/ssh_config via go mod tidy.
Cache-impact: none — desktop bindings only, no prompt/tool/provider path.
Cache-guard: n/a.
* feat(desktop): remote SSH frontend (host manager, explorer, ports, server)
- lib/types.ts: Remote* TS mirrors + SettingsTab "remote".
- lib/bridge.ts: 25 Remote* AppBindings + browser-dev mock + onRemoteStatus/
onRemoteForwards/onRemoteServer subscribers with a mock fan-out for tests.
- store/remote.ts: zustand store; pending_hostkey status drives the global
fingerprint dialog and resolves on the next non-pending status.
- RemoteHostsPage (Settings > Remote SSH tab): host list with status chip,
connect/disconnect/open, add/edit form, ssh_config import picker.
- RemotePanel: overlay drawer with Files (lazy tree + textarea editor with
mtime-conflict Reload/Overwrite), Ports (live forward rows + add form), and
Server (bootstrap phases + open-in-browser + logs) tabs.
- RemoteHostKeyDialog: global TOFU modal off the store's pendingFingerprint.
- StatusBar remote chip (worst live state, click opens explorer); App.tsx
subscribes remote:* app-wide and renders the panel + dialog.
- i18n keys added to en / zh / zh-TW (key sets stay aligned).
- Editor is a monospace textarea (no CodeMirror/Monaco) per lean-deps.
- Tests: src/__tests__/remote-store.test.ts (added to the test script).
Cache-impact: none — desktop frontend only.
Cache-guard: n/a.
* docs: document the Remote SSH module (SPEC, GUIDE, CONFIG_PATHS, CHANGELOG)
Adds the internal/remote layout + acyclic dependency note to SPEC 2, a bilingual
Remote SSH section to GUIDE.md / GUIDE.zh-CN.md (config, CLI, reconnect/persistence
semantics, host-key TOFU, remote-side state layout, desktop UI), the managed
known_hosts path to CONFIG_PATHS(.zh-CN), and a CHANGELOG entry.
Cache-impact: none — docs only.
Cache-guard: n/a.
* chore: ignore stray .reasonix state under package dirs
A bootstrap test whose RealPath resolved to CWD once wrote a .reasonix/remote/
token under the package dir and it was swept into a commit. The offending file
has been scrubbed from history; this guards against recurrence.
* fix(remote): CI green — errcheck + cross-platform bind-busy and Windows test skips
- client.go: check cl.Wait() return (errcheck lint).
- forward: classify WSAEADDRINUSE as bind-busy on Windows (build-tagged
addrinuse_windows.go), so ErrBindBusy is reported cross-platform.
- sftpfs / bootstrap integration tests skip on Windows: their in-process SFTP
server serves the LOCAL filesystem, so on Windows it serves drive paths and
the POSIX-remote path translation breaks — a harness limitation, not a
product one (V1 remotes are Linux/macOS). Linux/macOS CI covers the flows.
Cache-impact: none — test/lint-only + a Windows errno check.
Cache-guard: n/a.
* test(remote): skip EnsureServe reuse/relaunch/stop tests on Windows at setup
TestEnsureServeReusesLiveProcess and siblings pre-write state via pathsFor()
(POSIX path.Join) before newFakeConn runs, so on Windows the temp-dir remote
home yields a malformed drive path and the setup os.WriteFile fails. Move the
Windows skip to the top of each such test (shared skipOnWindows helper) so it
fires before any pathsFor/os setup, not just inside the harness ctor.
* fix(remote): address review — P1 security / data-integrity / lifecycle
Resolves the 14 findings from code review (all verified against the code):
Security
- Serve token no longer leaks to the remote log: runServe suppresses the
token share line under --port-file (supervised), and the launch script
creates the log 0600 (umask 077 + chmod). (F1)
- ProxyJump no longer sends the target's password to jump hosts: each hop gets
its own credential-free AuthOptions with a per-hop cache (persisted on the
Client so reconnects don't re-prompt). (F2)
- CLI surfaces a misconfigured proxy instead of silently dialing direct; the
https-proxy path now TLS-wraps the connection before CONNECT; desktop passes
the proxy Dialer it previously omitted. (F14)
Data integrity
- `remote fs get` streams the full file via sftpfs.Download instead of the
4 MiB-capped preview reader that silently truncated. (F3)
- Workspace slug carries an FNV hash of the exact path, so /srv/a-b and
/srv/a/b no longer share serve state files. (F10)
- Desktop host edit merges: passphrase_env/password_env/forwards (absent from
the desktop input) are preserved instead of wiped. (F7)
Lifecycle / correctness
- Bootstrap gates on `serve --help` advertising --port-file, not a version
number (the flags ship in this change, so no released version qualifies). (F4)
- Launch uses setsid only when present, so stock macOS (no setsid) works. (F5)
- Desktop no longer uploads the GUI binary as the remote CLI (LocalBinary=""
makes the upload path fail clearly toward npm). (F6)
- Serve tunnel re-binds to the current serve address on every EnsureServer and
is torn down on Stop; per-host serveMu serializes concurrent ensures. (F8)
- Reuse/Stop verify the pid is actually a reasonix serve (ps args), not just
kill -0, so a recycled PID is never mistaken or signalled. (F9)
- Desktop connect retries after failure (failed Start drops the managed host)
and builds a fully-populated host under the lock so a concurrent Disconnect
can always close it. (F11)
- SSH handshake is bounded by a conn deadline (ClientConfig.Timeout does not
cover NewClientConn), so a banner-less host can't hang the dial or Close. (F12)
- ScanSSHConfig returns a non-nil slice so an empty result encodes as [] not
null. (F13)
Tests: per-hop auth isolation, full-file download vs preview cap, slug
collision (/srv/a-b vs /srv/a/b), serve-alive/launch/locate command shape,
desktop edit-merge and non-nil scan. go test ./... green in both modules;
gofmt/vet clean; Windows cross-build green; cache-guard pass.
Cache-impact: none — no prompt/tool/provider/memory path touched.
Cache-guard: ./scripts/cache-guard.sh — all cases pass.
System-prompt-review: not required — no system-prompt-feeding config field changed.
* Harden Remote SSH lifecycle and packaging
Problem:
Remote SSH could misapply ProxyJump credentials, race connection and service lifecycles, interrupt live forwarding during replacement, and ship Desktop builds without the CLI sidecar required by upload installation. Several UI failures were also silent or left stale state behind.
Root cause:
Jump aliases shared endpoint-keyed authentication, bootstrap ownership relied on weak state checks without cross-process serialization, and Desktop operations lacked generation-scoped cancellation. Forward replacement stopped the working listener before the replacement was ready, while packaging and updater contracts did not include the CLI binary.
Fix:
Resolve and authenticate every SSH hop independently, make dialing and Desktop operations context-aware, serialize bootstrap with an atomic remote lock, verify exact process ownership, and make forward replacement failure-atomic. Harden token, SFTP, agent, config, and ssh_config handling; package and update the CLI sidecar on macOS, Windows, and Linux; and surface actionable validation and lifecycle errors in the Desktop UI.
Verification:
- go test ./...
- cd desktop && go test ./...
- go test -race for Remote, CLI, store, and Desktop modules
- go vet ./... for root and Desktop modules
- pnpm test:all and pnpm build
- Windows amd64 cross-compilation for Remote, bootstrap, forwarding, SFTP, and CLI packages
- clean merge snapshot against latest origin/main-v2 with root, Desktop, and frontend checks passing
* Add native Remote SSH workspace windows
Problem:
Remote SSH was buried in settings, and opening a remote workspace handed the authenticated loopback URL to the system browser instead of preserving a desktop workspace experience.
Root cause:
The desktop had no window-level Remote SSH control and no isolated Wails launch mode for a second native workspace window.
Fix:
Add a VS Code-style status entry, host switcher, command actions, and Remote dock. Launch remote workspaces in a lightweight Reasonix process that skips local session ownership, single-instance focus, tray services, and Wails bindings. Pass the loopback URL through a one-shot permission-restricted ticket so the token never appears in process arguments.
Verification:
- go test ./...
- pnpm test:all
- pnpm build
- wails build -clean
- native macOS multi-window smoke test
- git diff --check
* Tidy Remote SSH module metadata
Record the SSH config and SFTP packages as direct dependencies after rebasing onto the latest main-v2 baseline. Remove stale module checksums so the root module remains tidy in CI.
* Fix Remote SSH Windows test assumptions
Load the fixture SSH config by its explicit path while setting both platform home variables, and use the Windows .exe suffix for the desktop CLI fixture. This keeps the Remote SSH coverage portable across the CI matrix.
* fix(desktop): make session image and PDF exports reliable
Problem:
Desktop image and PDF exports could silently fail, and long sessions exceeded practical canvas limits.
Root cause:
SVG foreignObject content loaded through a blob URL tainted the export canvas in older embedded Chromium/WebView builds. The previous implementation also rasterized the entire session into one oversized bitmap.
Fix:
Render self-contained SVG data URLs, neutralize external resources, split long PNG exports at natural content boundaries, and build PDFs from independently rasterized pages. Surface success and failure feedback and add frontend and Go regression coverage.
Verification:
- pnpm test
- pnpm test:typecheck
- pnpm typecheck
- pnpm build
- cd desktop && go test ./...
- browser smoke test with a 34k-token session
* Harden multi-part session exports
Problem:
Long PNG exports could overwrite unconfirmed numbered sibling files, and external Markdown images loaded before sanitization could shift the measured export layout.
Root cause:
The frontend derived and wrote image parts sequentially, while media sanitization only ran on cloned DOM after the source surface had already rendered.
Fix:
Move multi-part image persistence into an exclusive staged backend batch with rollback, and sanitize initial Markdown image rendering to an allowlist of inline raster data before waiting for image decode and pagination.
Verification:
go test ./... in the root and desktop modules; go vet ./... in both modules; pnpm test; pnpm test:typecheck; pnpm typecheck; pnpm build; Wails binding generation.
* Harden final session export edge cases
Problem:
Image and PDF export could still alter a native-dialog path, publish files with inconsistent modes, remove a replaced target during rollback, append a blank trailing page, filter safe inline images, expose selected directories in errors, and retain all PNG blobs during base64 conversion.
Root cause:
The multipart path was normalized after selection, commit identity was sampled too late, temporary-file defaults leaked into published files, pagination treated bottom padding as content, react-markdown applied its default data-URL filter, and image encoding accumulated both blob and base64 representations.
Fix:
Preserve selected paths, stage payloads privately with normal final permissions, track the staged inode before publication, sanitize filesystem errors, plan slices through meaningful content, admit only strict raster base64 image URLs, and encode image parts one at a time.
Verification:
- go test ./... and go vet ./... in root and desktop
- go test -race ./... in desktop
- pnpm test, pnpm test:typecheck, pnpm typecheck, and pnpm build
- Windows amd64/arm64 desktop test compilation
- Chromium UI image/PDF export smoke tests
- macOS WKWebView multi-slice PNG/PDF artifact verification
* Fix merged theme test window mock typing
Problem:
The latest main-v2 theme test narrows window.go with an intersection, but the existing global declaration keeps App typed as the complete AppBindings interface, so test:typecheck rejects the one-method mock.
Root cause:
Intersecting with typeof window cannot replace an already-declared nested property type.
Fix:
Cast through unknown for this isolated test mock so its intentionally partial bridge shape is checked against the local test contract.
Verification:
- pnpm test:typecheck
- pnpm exec tsx src/__tests__/theme-pack.test.ts
- pnpm exec tsx src/__tests__/session-export.test.ts
* fix(desktop): add WebKit2GTK 2.50 SIGUSR1 signal workaround for Linux crash
WebKit2GTK 2.50.x changed internal GObject type registration in ways that
interact badly with the Go runtime's SIGUSR1 signal handler, causing a
SIGABRT inside webkit_user_content_manager_new() on certain Linux
distributions (Deepin 23, Arch). The crash produces no useful error
output — just a white window that exits with code 1.
Root cause: Go's runtime uses SIGUSR1 (signal 10) for thread management,
and JavaScriptCore also uses SIGUSR1 for conservative GC stack scanning.
When Go registers its handler first, WebKit's attempt to set its own is
overridden, corrupting the GObject type system during webview setup.
This commit adds two workarounds applied via package init() before Wails
initialises the webview:
1. JSC_SIGNAL_FOR_GC=12 — tells JavaScriptCore to use SIGUSR2 instead of
SIGUSR1, eliminating the signal conflict at the source.
2. WEBKIT_DISABLE_DMABUF_RENDERER=1 on NVIDIA GPUs — the DMA-BUF
compositing path in WebKit2GTK 2.50.x is unstable with NVIDIA's
proprietary driver; disabling it falls back to the stable software
OpenGL path.
Also bumps the Wails dependency from v2.12.0 to v2.13.0 for additional
Linux/WebKit compatibility fixes from the upstream.
Tested on: Deepin 23, WebKit2GTK 2.50.4, Go 1.26.5, NVIDIA GT 1030
(driver 580.119.02), X11 session.
Cache-impact: none
Cache-guard: none (desktop/init only, no prompt/tool prefix changes)
System-prompt-review: none
* fix(desktop): keep WebKit workaround Linux-only
Problem:
The PR coupled a Linux WebKit compatibility workaround to a cross-platform Wails v2.13.0 upgrade with placeholder checksums, and the new Linux file redeclared an existing NVIDIA helper.
Root cause:
The dependency bump expanded the change beyond the affected platform, while the compatibility file duplicated platform support already owned by nvidia_wayland_linux.go.
Fix:
Restore Wails v2.12.0 and its verified checksums, reuse the existing Linux NVIDIA detector, and format the compatibility file.
Verification:
- go mod tidy
- go build ./...
- go test ./...
- git diff --check
Co-authored-by: SivanCola <[email protected]>
* fix(desktop): backport Linux WebKit signal repair
Problem:
Wails v2.12 repairs Linux signal handlers only once, before JavaScriptCore may lazily replace them, so affected WebKitGTK applications can still abort during startup.
Root cause:
JavaScriptCore installs handlers after the one-shot repair and can drop the SA_ONSTACK flag required by the Go runtime. Moving JSC GC to SIGUSR2 does not restore those flags and is not the upstream fix.
Fix:
Backport the final Wails v2.13 Linux strategy: reapply SA_ONSTACK every 50 ms for a bounded five-second startup window and once again at DomReady, while leaving SIGUSR1 under JavaScriptCore ownership. Keep Wails v2.12 and retain the NVIDIA DMA-BUF workaround. Non-Linux hooks are no-ops.
Upstream: wailsapp/wails#5507, wailsapp/wails#5530
Verification:
- go test ./... (desktop)
- go build ./... (desktop)
- go vet ./... (desktop)
- go test ./... (repository root)
- GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build ./... (desktop)
- GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build ./... (desktop)
- git diff --check
* Scope Linux WebKit renderer fallback to safe mode
Problem:
All Linux NVIDIA launches inherited WEBKIT_DISABLE_DMABUF_RENDERER, disabling accelerated backing store, while the signal-repair comments implied coverage of failures before the GTK main loop.
Root cause:
The renderer fallback ran from package initialization without recovery gating. The backported GLib timer only dispatches after GTK enters its main loop.
Fix:
Apply the DMA-BUF fallback only to Safe Mode NVIDIA launches before Wails creates WebKit, preserve preconfigured environment values, keep other platforms as no-ops, and document the signal fix boundary. Add focused policy and platform tests.
Verification:
- cd desktop && go test ./...
- cd desktop && go vet ./...
- cd desktop && go build ./...
- cd desktop && GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build ./...
- git diff --check
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* Fix LongCat context window presets
Problem:
LongCat-2.0 presets treated the 131,072-token maximum output as the full context window, causing Reasonix to compact conversations far too early.
Root cause:
The preset copied the API max_tokens limit instead of the model context_length metadata.
Fix:
Use the official 1,048,576-token context length for both LongCat transports and migrate only untouched official preset entries that still carry the legacy value. Preserve custom providers and user-edited context windows.
Verification:
- ./scripts/cache-guard.sh
- go test ./internal/config
- go test ./...
* Narrow LongCat preset migration
Problem:
The legacy-context migration could rewrite a customized LongCat preset that retained the old provider-wide value but exposed a different or additional model.
Root cause:
The migration matched only preset identity, kind, host, and context value; it did not verify the original endpoint and LongCat-2.0-only catalog.
Fix:
Require the exact normalized endpoint, original single-model catalog, and explicit default model. Add regression coverage for customized catalogs, endpoints, and defaults, and keep load coverage independent of implicit persistence.
Verification:
- ./scripts/cache-guard.sh
- go test ./internal/config
- go test ./...
- go vet ./...
* feat(desktop): add pane opacity control and fix base style revert on custom theme save
New feature — per-scene pane opacity (面板透明度):
- ThemePackBackground / ThemePackSceneBackground gain a paneOpacity field
(0-1) that controls how transparent the shell panels render when a theme
pack is active. Home defaults to 0.50, task defaults to 0.68.
- Independent sliders in ThemeGallery editor for both scenes.
- applyBackgroundCSSVars pre-computes CSS variables (--theme-pane-alpha,
--theme-pane-shell-pct, --theme-pane-task-*) for color-mix rules.
- CSS rules under :root[data-theme-pack] apply pane opacity to sidebar,
topicbar, dock, statusbar, chat-pane, composer-card, and transcript
with cascading opacity tiers (shell < card < inner).
- ThemePreviewSurface preview cards now reflect pane opacity.
- Task background opacity caps relaxed from 0.45 to 1.0 per user demand.
- All 8 official mock themes include paneOpacity: 0.50.
- Schema and docs (EN/ZH) updated with paneOpacity field.
Bug fix — base style revert on Save & apply:
When ThemeGallery.saveEditor(activate=true) called activateThemePack(),
the downstream onExperienceChange callback passed exp.baseStyle (the
config-file appearance, e.g. graphite) to the parent's onThemeStyle.
The parent then called applyTheme(theme, OLD_style) which overwrote
the pack's baseStyle (e.g. refined) that applyThemePack had just set
on the DOM.
Fixes:
- AppearanceOverview.onExperienceChange now syncs exp.effectiveStyle
instead of exp.baseStyle, and no longer calls onTheme (which reads
stale themeStyle from its closure).
- SettingsPanel.onThemeStyle now reads the current theme from the DOM
via getTheme() instead of the React closure theme state.
- ThemeGallery.saveEditor moves cancelThemePreview() to only the
non-activate branch; the activate path must not restore the preview
snapshot after the new pack is applied.
- themeExperience.activateThemePack calls clearPreviewSnapshotOnly()
first so endPreviewIfAny() cannot overwrite the newly activated pack.
- Layout transparency CSS selectors gain .app:not(.app--creation) to
exclude the creation-mode surface.
Test contract:
- TestTaskOpacityCapped renamed to TestOpacityClamped, now validates
the new 0-1 clamping and PaneOpacity defaults for both scenes.
- 'layout background transparent' contract test updated to a single
flexible regex that tolerates intermediate selectors.
- i18n: taskBackgroundHint adjusted (removed stale 45% cap reference);
new paneOpacity label in EN/ZH/ZH-TW.
* fix(desktop): preserve theme pane transparency state
Problem:
- Explicit 0% pane opacity was replaced by legacy defaults.
- Token-only theme packs received background transparency rules.
- Failed theme activation discarded the reversible preview snapshot.
Root cause:
- A scalar JSON field could not distinguish omission from an explicit zero.
- Extended pane selectors were scoped to any theme pack instead of packs with backgrounds.
- The preview snapshot was cleared before persistence succeeded.
Fix:
- Represent pane opacity as an optional persisted value and preserve explicit zero through normalization and JSON round trips.
- Guard background and pane transparency CSS with data-theme-has-bg.
- Commit preview state only after successful activation and add focused regressions.
Verification:
- cd desktop && go test ./...
- cd desktop/frontend && pnpm run test
- cd desktop/frontend && pnpm run build
- Browser DOM/CSS checks for token-only and background theme previews
* fix(desktop): separate theme base state from pack effects
Problem:
- Applying a theme pack mirrored its effective style into the configured base appearance, so direct theme reset restored the pack style.
- Pane opacity previews changed while live card surfaces remained fixed at 88%.
Root cause:
- The gallery callback reused the configured-style owner callback for a temporary effective style.
- Card-tier CSS variables were computed but not consumed, and preview offsets differed from live formulas.
Fix:
- Synchronize configured base style only when no active pack owns the effective style.
- Consume scene-specific card opacity variables in live surfaces and use the same formulas in previews.
- Remove unused pane variables and add regressions for direct reset and opacity consumers.
Verification:
- cd desktop/frontend && pnpm run test
- cd desktop/frontend && pnpm run build
- cd desktop && go test ./...
- Browser: Sage/Slate reset restored Graphite; live and preview card opacity matched scene variables
* fix(desktop): preserve theme preview lifecycle
Problem:
- Save and apply closed the theme editor before activation completed, allowing unmount cleanup to restore the old preview snapshot.
- Base and token-only gallery previews received background pane transparency that live surfaces do not apply.
Root cause:
- Editor teardown occurred before activateThemePack committed the preview.
- Preview opacity variables were derived without checking whether the pack had a background.
Fix:
- Activate or cancel the preview before closing the editor and reloading the gallery.
- Keep pane previews opaque when no theme background is present.
- Add regressions for lifecycle ordering and preview opacity.
Verification:
- cd desktop/frontend && pnpm run test
- cd desktop/frontend && pnpm run build
- cd desktop && go test ./...
- Browser preview check: base previews stayed opaque while background previews retained configured alpha
* fix(desktop): keep theme activation and previews consistent
Problem:
- Save and apply persisted activation twice, so a later activation failure could split persistent state from the reversible preview.
- Browser official-theme mocks used 50% pane opacity while embedded manifests fell back to legacy defaults.
- Legacy task scene payloads without pane opacity did not inherit the home setting.
Root cause:
- ThemeGallery delegated activation to SaveThemePack and then called the experience activation controller again.
- Official manifests omitted the new field, and the task fallback tested object presence instead of field presence.
Fix:
- Make the experience controller the sole owner of active-theme pointer updates after saving.
- Declare 50% pane opacity in every official manifest and verify Wails/browser parity.
- Use nullish field fallback for legacy task scenes and add regression coverage.
Verification:
- pnpm exec tsx src/__tests__/theme-pack.test.ts (230 passed)
- pnpm run typecheck
- pnpm run check:css
- pnpm run test
- pnpm run build
- go test ./...
---------
Co-authored-by: SivanCola <[email protected]>
* fix(desktop): prevent cursor stuck at resize shape when window is maximized
When the Wails frameless window is restored maximized, the 6px edge-detection
zone in useWailsResizeFix coincides with the physical screen edges. Moving
the mouse to the top/right/top-right corner sets style.cursor to a resize
value that WebView2 never reliably reverts via style.cursor = "", leaving
the cursor stuck in resize shape until the app is restarted.
The fix adds a synchronous check at the top of the mousemove handler:
when outerDimensions ≈ screen.availDimensions (within 5px tolerance) the
window is maximised, so edge detection is skipped entirely and any stale
resize edge is cleared via removeProperty("cursor").
Fixeswailsapp/wails#1503 (same bug, project-specific manifestation).
* fix(desktop): use native maximized state for resize cursor
Problem:
Maximized Windows desktop windows could retain a resize cursor, while near-full normal windows could be misclassified as maximized by the size heuristic.
Root cause:
Cursor recovery was gated on Wails resizeEdge state, and maximization was inferred from outer and screen dimensions instead of the native window state.
Fix:
Share the native maximized state with the Windows controls, normalize stale resize cursors, preserve the remembered default cursor, and skip edge detection only for confirmed maximized windows. Add focused startup, transition, and near-full-size regression coverage.
Verification:
- pnpm exec tsx src/__tests__/wails-resize-fix.test.tsx
- pnpm exec tsx src/__tests__/app-chrome-tabs.test.ts
- pnpm run typecheck
- pnpm test
- pnpm run build
- git diff --check
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* feat: add configurable conversation width (standard 960px / full 90%)
* fix: correct render.go conversation_width syntax (t+&+literal newline)
* fix: gofmt -w desktop/settings_app.go
* Fix conversation width behavior and persistence
Problem:
- Full width could become narrower than standard in common window sizes and compounded inside nested transcript containers.
- The Appearance panel could display stale localStorage state instead of the persisted desktop setting.
Fix:
- Apply a 960px floor to full width and keep nested transcript content at 100% of its outer container.
- Treat localStorage as an early-paint cache, then reconcile from normalized desktop settings.
- Centralize config normalization and add frontend, config, render, and desktop bridge regressions.
Verification:
- pnpm test:all
- pnpm build
- go vet ./... and go test ./... in the root module
- go vet ./..., go build ./..., and go test ./... in the desktop module
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SauronSkywalker <[email protected]>
Co-authored-by: SivanCola <[email protected]>
* feat(cli): support PRIMARY selection paste on middle-click
Add pastePrimary() that reads from the X11/Wayland PRIMARY selection
(via wl-paste --primary, xclip -selection primary -o, or xsel
--output --primary) and wire it to the MouseMiddle event in the TUI.
Previously, with mouse capture enabled, middle-click was consumed but
not handled, silently breaking the terminal convention of PRIMARY paste.
Now it works correctly: PRIMARY content is pasted into the composer, and
an empty PRIMARY is a silent no-op.
* fix(cli): paste tmux buffer on middle click
* fix(cli): harden middle-click paste handling
Problem:
Middle-click paste could expose credential-store variables to clipboard helpers, mutate a hidden composer, read PRIMARY on an SSH host, and alter selected text.
Root cause:
The new subprocesses inherited the full process environment, while the mouse and PRIMARY paths bypassed existing ownership, remote-session, and byte-preservation contracts.
Fix:
Filter every paste helper through secrets.ProcessEnv, respect tmux/SSH/composer boundaries, request text explicitly, preserve selection bytes, and add focused regression coverage.
Verification:
- go test ./internal/cli
- go test -race ./internal/cli
- go vet ./...
- git diff --check
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
* agent/save: fix Windows crash when model name contains ':'
Fixes#6683
NewSessionPath() only sanitized '/' and '\' from model names, missing ':'
and other Windows-illegal filename characters. On Windows, ':' is the
NTFS alternate data stream (ADS) marker, causing:
- System freeze / crash when file system scanners index the session file
- CLI permanently stuck: lease.lock path with ':' triggers 'session is
in use' error that can never clear
- All sidecar files (.ckpt, .lock, .lease.*, .events.jsonl) corrupted
Add all Windows-illegal chars to the replacer: :<>"|?*
* test(agent): cover Windows-safe session filenames
Problem:
The Windows filename fix had no regression covering a real colon-bearing model label or persistence through the generated path.
Fix:
Add a focused test for the curated nemotron-3-nano:30b model shape, cover the newly sanitized punctuation, and save plus stat a session through the real persistence path.
Compatibility:
The test exercises future filename derivation only. Existing session files and safe model labels remain unchanged.
Verification:
- go test ./internal/agent -run TestNewSessionPath -count=1
- go test ./internal/agent ./internal/control ./internal/cli ./internal/boot
- cd desktop && go test ./...
- git diff --check
Co-authored-by: SivanCola <[email protected]>
---------
Co-authored-by: SivanCola <[email protected]>
Fixes#6683
NewSessionPath() only sanitized '/' and '\' from model names, missing ':'
and other Windows-illegal filename characters. On Windows, ':' is the
NTFS alternate data stream (ADS) marker, causing:
- System freeze / crash when file system scanners index the session file
- CLI permanently stuck: lease.lock path with ':' triggers 'session is
in use' error that can never clear
- All sidecar files (.ckpt, .lock, .lease.*, .events.jsonl) corrupted
Add all Windows-illegal chars to the replacer: :<>"|?*
* fix: harden crash diagnostics and runtime failures
Problem:
Crash telemetry mixed actionable release failures with browser notices, development reports, and isolated timer spikes. Several recurring desktop crashes remained in virtualized React lists, Mermaid pan/zoom, legacy WebKit, and concurrent secret redaction.
Root cause:
Virtualizer measurement updates could re-enter React commits, svg-pan-zoom inverted singular matrices before layout stabilized, react-markdown assumed Object.hasOwn, and the key/value redactor relied on a complex regexp replacement path. Opaque Script errors also lacked safe grouping context.
Fix:
Separate diagnostic lanes and effective severities, preserve legacy fingerprints while adding optional safe hints, require sustained or corroborated lag, use direct DOM virtualizer updates, guard Mermaid layout synchronization, polyfill Object.hasOwn, and replace the backtracking redactor with a linear scanner.
Verification:
go test ./...
go vet ./...
go test -race ./internal/secrets
(cd desktop && go test ./...)
(cd desktop/frontend && pnpm test:all && pnpm build)
(cd workers/crash-report && npm test -- --run && npm run typecheck)
* fix: isolate development crash groups
Problem:
Development and stable reports shared one fingerprint, so a low-severity development observation could hide an actionable release crash.
Root cause:
The fingerprint omitted the report environment while the group upsert persisted low severity and dashboard classification trusted the latest version/channel.
Fix:
Keep stable hashes unchanged, namespace development fingerprints with dev:, preserve dashboard routes for both formats, and only reclassify legacy groups when both ends of their history are development builds.
Verification:
- npm test -- --run
- npm run typecheck
- npm exec -- wrangler deploy --dry-run --outdir /tmp/reasonix-pr-6678-worker-dryrun
- git diff --check
* fix: keep ambiguous legacy crash groups actionable
Problem:
Legacy crash groups could contain a stable observation between development reports and still be downgraded as development-only.
Root cause:
The dashboard inferred complete group provenance from first/last version metadata even though sample retention can prune middle release reports.
Fix:
Treat only the explicit dev: fingerprint namespace as proof of a development-only group, keep legacy hashes actionable, and cover dev-release-dev history in regression tests.
Verification:
- npm test -- --run
- npm run typecheck
- npm exec -- wrangler deploy --dry-run --outdir /tmp/reasonix-pr-6678-worker-dryrun
- git diff --check
Problem: Shared provider endpoints can expose models with different context limits, but Reasonix only allowed one provider-wide context_window value.
Approach: Add an optional model_overrides.<model>.context_window, preserve existing capability overrides, expose the setting in desktop UI with token guidance and a low-value warning, and document the behavior.
Compatibility: Missing or zero model overrides inherit the provider value. The desktop JSON field is optional, and older TOML readers ignore the additive nested key.
Cache impact: No provider-visible prompt or schema bytes change. Explicitly smaller values may compact earlier; the UI warns below 16384 tokens and unset values preserve existing behavior.
Verification: ./scripts/cache-guard.sh; go vet ./...; go test ./...; cd desktop && go test ./...; frontend typecheck, CSS checks, and provider model tests.
Co-authored-by: HaoyueQin <[email protected]>
Problem: A competing process holding the compatibility session save lock
wedged desktop navigation and true-quit shutdown indefinitely, kept the
session lease and WebView alive, and a timed-out final snapshot could
discard transcript changes that existed only in memory (#6637, Bug 3).
Root cause: flock/LockFileEx used blocking acquisition with no deadline;
the shutdown path treated a held lock like any other snapshot failure;
and during the two-phase recovery handoff a concurrent tab-layout save
could overwrite the recovery anchor with the controller's stale path.
Fix: Retry non-blocking lock acquisition for a bounded five-second window
on Unix and Windows and keep the in-memory transcript on failure; on a
true shutdown timeout persist the transcript to a process-specific
recovery branch that never re-acquires the stalled original lock, reusing
the standard session/event-log/branch-meta formats and recovery handoff;
make the lease-backed tab path authoritative during the handoff via a
lock-free lease-key mirror so layout saves never wait on lease file
operations.
Verification: go test -race ./internal/agent ./internal/control; cd
desktop && go test -race ./...; go vet ./... (root and desktop modules);
trial merge with origin/main-v2 clean; scripts/check-cache-impact.sh
reports no cache-sensitive files changed; full CI green including race,
desktop-windows, cache-impact, and CodeQL.
Problem:
Stopping a Desktop turn discarded the streamed assistant/reasoning/tool output
after a reload or tab switch, and a cancelled plan-mode prompt could carry
stale mode instructions or lose the current user message entirely.
Root cause:
Streaming cards lived only in frontend memory; cancellation correctly removed
the incomplete tail from the provider transcript but left no durable display
buffer, and display state was owned by transient tab wrappers.
Fix:
Mark user-cancelled TurnDone events, preserve a prefix-free current user
fallback with image attachments, buffer display output in runtime-owned shared
state across detach/reattach, persist cancelled executor output in the
display-only planner sidecar guarded by a bounded cross-process file lock,
retry failed writes in order, remove or prune sidecar entries on permanent
deletion and orphan cleanup, and show a localized notice explaining that
partial output is not model context.
Verification:
Root and desktop Go suites, focused -race tests, deterministic cross-process
subprocess regression, allocation regression, Windows cross-compilation,
frontend typecheck and controller meta tests, and scripts/cache-guard.sh pass.
Fixes#6260Fixes#6297Fixes#6644
Co-authored-by: GTC2080 <[email protected]>
Co-authored-by: SauronSkywalker <[email protected]>
Problem:
The shared test user-state guard left inherited platform cache roots untouched, so tests could still reach caller-owned directories on Linux and Windows.
Root cause:
The guard redirected home and config roots but did not cover every environment variable consulted by os.UserCacheDir.
Fix:
Redirect XDG cache and state roots plus Windows LocalAppData into the disposable home. Verify redirection, restoration, and the actual config paths that depend on the OS cache root.
Verification:
- go test ./internal/testenv ./internal/config -count=1
- go test -race ./internal/testenv ./internal/config -count=1
- go test -p=1 ./... -count=1
- go vet ./...
- ./scripts/check-cache-impact.sh
Problem: The quoted Windows batch fix re-serialized every parsed argument with double quotes, changing raw batch parameters such as %1 for hooks that were already valid.
Root cause: The compatibility path matched every simple batch shell command and discarded the original shell-form argument text before constructing the cmd.exe command line.
Fix: Limit shell-form handling to the broken leading-quoted executable shape, preserve its argument tail verbatim, quote argv-form arguments only when required, and make the Windows runtime test assert raw %1 behavior.
Verification: go test ./internal/hook -count=1; go test -race ./internal/hook -count=1; go vet ./internal/hook ./internal/pluginpkg ./internal/config; GOOS=windows GOARCH=amd64 go test -c ./internal/hook; go test -p 4 ./...; cd desktop && go test -p 4 ./...; ./scripts/check-cache-impact.sh.
Problem:
The full race job found the steer fallback test reading Session.Messages while
the background turn appended to the same slice.
Root cause:
The polling helper bypassed the concurrency-safe Session.Snapshot API.
Fix:
Read a locked session snapshot before inspecting user messages.
Verification:
- go test -race ./internal/control -run TestSteer -count=20
- go test -race ./internal/control -count=1
Context:
- PR #6664 recorded the #6560 author as a repository co-author and also added permanent attribution text to the changelog and bilingual READMEs.
Change:
- Restore CHANGELOG.md, README.md, and README.zh-CN.md to their exact pre-#6664 content.
- Keep the merged #6664 commit and its Co-authored-by attribution in Git history.
Verification:
- git diff --check
- Tree comparison against the first parent of #6664
- ./scripts/check-cache-impact.sh
Problem:
Tests outside the original CLI path could still inherit REASONIX_HOME,
REASONIX_STATE_HOME, or REASONIX_CACHE_HOME and mutate caller-owned state.
Root cause:
Isolation was duplicated per package and Windows/default path precedence allowed
inherited overrides to escape temporary homes.
Fix:
Add a shared process-level test environment guard, apply it to state-writing
packages, preserve helper subprocess behavior, and adopt Windows path
containment coverage from #6518.
Verification:
- go test -p=1 ./... -count=1 with external home/state/cache sentinels
- go test -race ./internal/testenv ./internal/config ./internal/cli -count=1
- go vet ./...
- ./scripts/check-cache-impact.sh
Co-authored-by: Lemon Hall <[email protected]>
Problem: Windows plugin hooks such as superpowers use a quoted run-hook.cmd path. Passing that command through Go's default cmd.exe argument encoding leaves the quotes in the command name, so SessionStart fails.
Root cause: Go's Windows argument encoder follows CommandLineToArgvW, while cmd.exe and batch files use different quote stripping rules.
Fix: Recognize simple .cmd/.bat hook invocations in both shell and argv forms, normalize the executable path separators, and provide an exact cmd.exe /d /s /c command line through SysProcAttr. Compound and non-batch shell contracts remain unchanged.
Verification: go test -p 4 ./...; cd desktop && go test -p 4 ./...; go test -race ./internal/hook; go vet ./internal/hook ./internal/pluginpkg ./internal/config; GOOS=windows GOARCH=amd64 go test -c ./internal/hook.
Problem:
- The contributor behind #6560 helped identify and solve the CLI clipboard compatibility gap, but the integrated #6632 commit did not record that co-contribution.
Root cause:
- The final implementation was merged independently without a contributor trailer or a permanent acknowledgment next to the shipped fix.
Fix:
- Credit Andrei Kiparuk in the changelog and bilingual README acknowledgments.
- Add a public Co-authored-by trailer so GitHub records the contribution after merge.
Verification:
- git diff --check
- ./scripts/check-cache-impact.sh
Co-authored-by: Andrei Kiparuk <[email protected]>
* feat(subagents): add profile-aware parallel fleets
Problem:
Custom subagent profiles could only be run serially through skill execution, making multi-document workloads unnecessarily slow and leaving concurrent writers without one session-wide safety boundary.
Root cause:
Task execution had no runtime profile selector or shared fleet scheduler, and write coordination was split across entry points without atomic parent reservations or enforceable path claims.
Fix:
- add profile-aware task and fleet execution with session-wide total/writer concurrency limits
- normalize and enforce write path claims, including sandboxed Bash roots and parent write reservations
- make background tasks queue asynchronously and keep parallel_tasks on the shared scheduler
- expose concurrency and read-only profile controls in Desktop and document the contracts
Verification:
- go test ./...
- cd desktop && go test ./...
- go test -race ./internal/agent ./internal/boot ./internal/config ./internal/skill
- cd desktop/frontend && pnpm typecheck && pnpm test:typecheck && pnpm test
- ./scripts/cache-guard.sh
* Tighten subagent write and tool boundaries
Problem:
Path-bound Bash retained sandbox cache and temporary-directory write lanes, wildcard tool allowlists could reject valid narrowed calls, and stale scheduler code blocked lint.
Root cause:
Bash root rebinding did not enable minimal-write confinement, tool lists were intersected before live-registry pattern expansion, and obsolete control flow remained after scheduler unification.
Fix:
Restrict rebound Bash to claim roots, expand both allowlists before intersection, remove stale scheduler parameters and helpers, and add focused execution regressions.
Verification:
- golangci-lint v2.12.2: 0 issues
- go test ./...
- go test -race ./internal/agent ./internal/tool/builtin ./internal/boot ./internal/config ./internal/skill
- cd desktop && go test ./...
- ./scripts/cache-guard.sh
* Preserve fleet cancellation results
Problem:
Cancelled fleet writers could be reported as skipped after they had started, and preflight conflicts could cite the wrong task numbers when read-only items were interleaved.
Root cause:
The cancellation path stopped draining terminal worker results after waiting, while write claims were compacted into a writer-only slice before indexed validation.
Fix:
Drain every started worker result after cancellation, reserve skipped for tasks that never started, and retain one claim slot per original fleet item. Add deterministic cancellation and mixed-task indexing regressions.
Verification:
- go test -race ./internal/agent -run '^TestFleet' -count=1
- go test ./internal/agent
- go test ./...
- golangci-lint v2.12.2: 0 issues
- ./scripts/cache-guard.sh
- git diff --check
* feat(desktop): add regional typography controls
Problem:
Users can only change one global font family and text scale, so conversation, composer, code, metadata, and navigation cannot be tuned independently. Assistant response styles can also override the selected conversation typography.
Root cause:
The frontend exposes a single global typography layer, while higher-specificity theme rules hardcode assistant response font properties.
Fix:
Add persisted, backward-compatible typography preferences and a detailed settings view for five semantic regions. Route each region through CSS custom properties, localize the UI, and make assistant responses inherit the conversation font and size.
Verification:
- pnpm --dir desktop/frontend exec tsx src/__tests__/typography-preferences.test.ts
- pnpm --dir desktop/frontend exec tsx src/__tests__/typography-overflow-contract.test.ts
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend check:css
- pnpm --dir desktop/frontend build
- Browser computed styles confirm matching user and assistant typography
* fix(desktop): honor regional typography settings
Problem:
Regional typography settings could be ignored by assistant replies, code/tool content, and metadata. The theme restore copy also implied a broader reset than it performs.
Root cause:
Root-scoped typography tokens were computed before descendant region scales, and later theme selectors overrode regional font values. Custom font input could also diverge from its normalized persisted value, while restore labels did not name the Graphite-only scope.
Fix:
Expose exact per-region size properties, recompute semantic typography tokens at each region, route late selectors through regional variables, normalize custom font input before state updates, and clarify/localize the Graphite restore copy while preserving detailed typography.
Verification:
- pnpm --dir desktop/frontend test
- pnpm --dir desktop/frontend test:typecheck
- pnpm --dir desktop/frontend build
- Browser checks confirmed user and assistant replies render at 24px and remain 24px after restoring the Graphite appearance.
* fix(desktop): align typography previews
Problem:
Supporting-text customization changed the effective default from 12px to 11px, and code/metadata previews did not match their applied sizes.
Root cause:
The metadata region used an 11px scaling base while the existing status token uses 12px. The preview also multiplied every region by a hardcoded 14px base.
Fix:
Align metadata with the 12px supporting-text base and give each preview region an exact size variable with the correct global fallback.
Verification:
- pnpm --dir desktop/frontend test
- pnpm --dir desktop/frontend check:css
- pnpm --dir desktop/frontend test:typecheck
- pnpm --dir desktop/frontend build
- Browser computed-style checks confirmed code and metadata previews match their configured sizes and metadata stays 12px when leaving follow-global.
* fix(desktop): scale diff text with global size
Problem:
Diff text stayed at 12.5px while the code and tools region followed a non-default global text size.
Root cause:
The diff fallback bypassed the global font scale and only read the exact regional override.
Fix:
Scale the legacy 12.5px diff base by the global text-size token until the code region is customized, and lock the behavior with a CSS contract test.
Verification:
- pnpm --dir desktop/frontend exec tsx src/__tests__/typography-overflow-contract.test.ts
- pnpm --dir desktop/frontend check:css
- pnpm --dir desktop/frontend test:typecheck
- pnpm --dir desktop/frontend build
* fix(cli): restore terminal interactions and transcript hierarchy
Problem:
- Captured mouse mode could copy selections but could not right-click paste clipboard text.
- Composer drag selections were visible but did not reliably reach the system clipboard.
- Non-Git workspaces reserved or misaligned an empty footer data band.
- Assistant answers lacked a distinct identity and spacing from reasoning and turn receipts.
Root cause:
- Mouse handling had no text-only clipboard read path, composer selection release stopped at local state, and footer/transcript rendering assumed fixed visual bands.
Fix:
- Route right-click clipboard text through the canonical paste event and auto-copy composer drag selections.
- Make optional footer telemetry compact and left-aligned outside Git repositories.
- Render assistant markdown with a Reasonix identity, stable indentation, replay support, and semantic spacing.
- Document the interaction changes and add focused regression coverage.
Verification:
- go test ./internal/cli
- go test ./...
- git diff --check
* fix(cli): clarify right-click paste over SSH
Problem: Captured right-click paste attempted to read the remote host clipboard during SSH sessions and the documentation implied it could access the user's local clipboard.
Root cause: The right-click path called the native clipboard reader without applying the remote-session boundary already used by clipboard copy.
Fix: Detect SSH sessions before reading clipboard text, preserve the composer, show a localized terminal-paste hint, and document local versus remote behavior.
Verification: go test ./internal/cli; go test ./...; go vet ./...; git diff --check.
* test(cli): isolate local clipboard cases from SSH
Problem: Four local clipboard interaction tests inherited SSH environment variables and failed when the suite ran on a remote development host.
Root cause: The tests stubbed native clipboard operations without explicitly selecting a local clipboard session.
Fix: Add a shared test helper that clears all supported SSH markers and use it in each local paste and drag-copy case.
Verification: Normal and simulated-SSH go test ./internal/cli -count=1; go test ./...; go vet ./...; git diff --check.
Problem:
The registry publish CTA rendered as a narrow vertical pill instead of a normal horizontal button.
Root cause:
The CTA reused the publish panel's reg-publish class and inherited the panel's asymmetric section padding.
Fix:
Give the toolbar CTA a dedicated class and add a source contract test that keeps the CTA and panel layout classes separate.
Verification:
- npm test (site): 21 passed
- npm run build (site): passed
- In-app browser: CTA renders at 82 x 45 px with 12 px 26 px padding
Problem:
The stable orchestrator reported success after approval even though the CLI, npm, and Desktop publication jobs were skipped through a transitive skipped dependency. The existing recovery dispatch also could not use a workflow fix without moving the already-published tags.
Root cause:
Each reusable publisher joined an orchestrated guard with a deliberately skipped standalone gate. The cache guard used an explicit status condition, but its descendants did not, so GitHub propagated the skipped ancestor through the dependency chain. Recovery also checked out and executed control scripts from the old release candidate.
Fix:
Add explicit success conditions to every affected publication stage, keep recovery authorization and verification on protected main-v2 while building the immutable historical tag SHA, and reject non-ancestor or mismatched tag sets. Add a stable postflight that verifies required CLI/Desktop release assets and the exact npm latest version so a no-op release cannot stay green.
Verification:
- bash scripts/release-workflows.test.sh
- go run github.com/rhysd/actionlint/cmd/[email protected] -ignore 'label "windows-11-arm" is unknown' on all release workflows
- bash -n on all changed release scripts
- verify-stable-release-artifacts.sh against public v1.17.14 artifacts
- ./scripts/cache-guard.sh
- go test ./...
- git diff --check
Release notes:
- add reviewed bilingual highlights, changes, upgrade notes, and risk notes
- ground every behavioral claim in merged pull requests from the v1.17.14 range
- include stable Desktop, CLI, MCP, agent, and provider release links
Verification:
- release catalog validation and rendering tests
- root and Desktop test, race, and vet suites
- frontend and site tests plus production builds
- cache guard and npm cross-platform dry run
- bounded live-provider tool, error-recovery, usage, and resume scenarios
- Linux, macOS, and Windows CI, CodeQL, and vulnerability checks
* site: add dark mode, trim decorative FX, unify cache-block motif
Motivation:
- The marketing site shipped light-only, which mismatches the
terminal-tool audience, and the hero stacked many decorative
effects at once (cursor glow, magnetic buttons, headline
typewriter, dot grid, 3D tilt, spotlight borders, marquee) with
no single signature interaction.
- The cache-block visual from "How it works" is a product-native
motif that was used only once.
Changes:
- Add a three-state theme switch (system/light/dark) shared by the
marketing pages, community pages, and changelog: a pre-paint
inline script reads localStorage "reasonix-theme", the shared
theme.js module wires the toggle, keeps meta[theme-color] in
sync, and follows OS theme changes while the preference is
"system".
- Flip every surface to tokens: hardcoded #fff surfaces become
var(--card), white color-mix tints mix into var(--bg)/var(--card),
nav blur backgrounds derive from var(--bg); global.css and
community.css gain full :root[data-theme="dark"] palettes.
- Remove cursor glow, magnetic buttons, and the h1 typewriter
(JS, CSS, and the Base.astro div); the dot-grid and terminal-tilt
rAF loops now run only while visible via IntersectionObserver.
- Reuse the cache-block motif: section dividers become a row of
cache blocks with a solid center, the 404 page gets a "gone cold"
block row, kickers/docs-kickers go monospace with a "$ " prefix,
and the hero headline highlights its keyword as a mono cache
block.
- Hide the header sign-in button below 640px so the theme switch
fits the mobile nav.
Verification:
- cd site && npm test: 14/14 pass.
- cd site && npm run build: 19 pages built; dist inspected for
[data-theme=dark] rules, theme-switch markup, and removed-FX
leftovers (none found).
* site: scale changelog format to the release cadence
Motivation:
- The changelog layout was designed for monthly flagship releases,
but Reasonix ships near-daily patches: every patch rendered a
full-ceremony hero, empty Upgrade/Risk sections stayed on the
page and in the TOC, the version rail listed every release flat,
and /changelog/ duplicated the latest version page under two
canonical URLs.
Changes:
- Render Guides/per-kind changes/Upgrade/Risk/Thanks sections only
when they have content, and drop them from the TOC accordingly;
section numbers are now sequential 01..N over the sections that
actually render (no more mixed 01/U/R/heart badges).
- Releases whose title is just the version string (patch releases)
get a compact hero: no duplicated version line, no action
buttons, summary as a plain lede. Thematic titles keep the full
hero.
- Group the version rail by minor (e.g. 1.17.x), showing the latest
three patches per group; older patches sit behind a native
<details> expander that auto-opens when it holds the current
version.
- /changelog/ now canonicalizes (and og:url) to the latest
version's /changelog/vX.Y.Z/ URL via an optional canonical prop
on Base.astro.
- Align the changelog palette with the shared tokens: release
blue/ink/backdrop alias to the global accent/ink/bg tokens and
inherit their dark flip, keeping only --release-paper (the ledger
identity) and the semantic orange/green as page-local tokens.
Verification:
- cd site && npm test: 14/14 pass.
- cd site && npm run build: /changelog/ canonical points to
/changelog/v1.17.14/; v1.17.14 renders compact with no empty
sections and gapless 01-06 numbering; v1.17.13 renders the full
hero with guides/upgrade/risks/thanks present.
* site: lead the download tabs with the desktop app
Motivation:
- The hero already presents the desktop app as the primary install
path, but the download section at the bottom defaulted to the npm
tab, so the two install narratives disagreed.
Changes:
- Reorder the download tabs to Desktop / npm / Homebrew and make
the desktop pane the default active one; pane markup is otherwise
unchanged, and ?dl= deep links plus data-goto jumps still
activate panes by name.
Verification:
- cd site && npm test: 14/14 pass.
- cd site && npm run build: 19 pages built; dist shows the desktop
tab first and active by default.
* site: require exact version-string titles for the compact changelog hero
Problem:
- A thematic release whose title starts with the version string
(e.g. "Reasonix 1.18.0 — Better agents") would be classified as a
patch release and lose its full hero, including the download and
GitHub actions.
Root cause:
- The classification regex /^reasonix\s+v?\d+\.\d+\.\d+/i had no
end anchor, so any title merely STARTING with the version matched.
Fix:
- Extract isThematicTitle() into site/src/scripts/release-title.js:
an exact, normalized comparison against "Reasonix <version>" and
"Reasonix v<version>" (trimmed, case-insensitive, collapsed
whitespace). Titles that only begin with the version stay
thematic; edge cases (empty title/version) default to the full
hero.
- Cover it with release-title.test.mjs, including the
"Reasonix 1.18.0 — Better agents" regression case.
Verification:
- cd site && npm test: 19/19 pass.
- cd site && npm run build: 19 pages; v1.17.14 stays compact,
v1.17.13 keeps the full hero.
* site: keep the header nav within 390px viewports
Problem:
- With the new theme switcher added, the header overflowed
horizontally at 390px: the marketing nav measured ~448px and the
install button was clipped by overflow-x: hidden; the community
nav measured ~428px with the theme switch partially clipped and
the account entry fully offscreen.
Root cause:
- The previous mitigation (hiding .nav-sign-in below 640px) only
freed ~80px, not enough once brand text, language switch, theme
switch, and install button all compete for 390px.
Fix:
- global.css ≤640px: logo-only brand (.nav .brand span hidden) and
narrower theme-switch buttons (9px padding), bringing the nav to
~360px.
- community.css: new ≤640px block hiding .brand .ctag, tightening
.nav-inner padding/gap, and narrowing theme-switch buttons,
bringing the nav to ~315px.
- Add mobile-nav-contract.test.mjs asserting these contraction
rules exist in the ≤640px blocks of both stylesheets (source-level
contract; the repo has no browser layout test harness).
Verification:
- cd site && npm test: 19/19 pass.
- cd site && npm run build: 19 pages built.
* site: keep mobile headers within narrow viewports
Problem:
- The community header fit at 390px only while the async account slot was empty; the signed-out link widened it to 406px and was clipped.
- The marketing header still overflowed at the 320px reflow boundary.
Root cause:
- The responsive contract did not budget for the post-load account control or the narrower mobile breakpoint.
Fix:
- Give the community brand a stable accessible name, collapse it to the mark below 440px, and tighten language, theme, account, and container spacing.
- Add a 360px marketing breakpoint that preserves every header control at 320px.
- Extend the source contract tests to cover the async account slot and narrow layout rules.
Verification:
- cd site && npm test: 20/20 passed.
- cd site && npm run build: 19 pages built.
- Browser checks at 390px and 320px showed no horizontal overflow after the account link loaded; desktop brand labels remained visible.
- git diff --check passed.
* ci(release): orchestrate stable publishing behind one approval
Problem:
Stable CLI, npm, and Desktop tags started independent workflows, so one stable release required repeated GitHub environment approvals and separate coordination.
Root cause:
Each publisher owned its stable tag trigger and protected environment instead of sharing one validated release decision.
Fix:
Add a stable release orchestrator that validates the atomic three-tag set, requests one environment approval, and invokes guarded reusable publishers in parallel. Preserve standalone prerelease and recovery gates, harden version resolution, and add workflow contract coverage and release documentation.
Verification:
- actionlint v1.7.7 on release workflows and CI
- bash scripts/release-workflows.test.sh
- ./scripts/cache-guard.sh
- node --test scripts/release-notes.test.mjs
- go test ./...
* fix(release): bind publishers to approved commit
Problem:
The reusable publishers trusted a caller path on any ref, rebuilt mutable tags after approval, and the new actionlint gate failed on an unquoted keychain list.
Root cause:
The orchestrator passed tag names but not its approved commit, while child guards did not require a protected caller ref or matching workflow SHA. The macOS keychain command also relied on shell word splitting.
Fix:
Require protected caller context, propagate the approved CLI tag and SHA, check out the immutable commit, and revalidate remote tags before publication. Preserve keychain arguments with a Bash array and document the repository-write trust boundary.
Verification:
- bash scripts/release-workflows.test.sh
- go run github.com/rhysd/actionlint/cmd/[email protected] on all release workflows
- ./scripts/cache-guard.sh
- node --test scripts/release-notes.test.mjs
- go test ./...
- git diff --check
* fix(ci): remove ripgrep dependency from release tests
Problem:
The release workflow contract test passed locally but failed in GitHub Actions because the lint runner does not provide ripgrep.
Root cause:
The test script used rg only for simple regular-expression assertions even though CI does not install that optional tool.
Fix:
Use portable grep -E assertions throughout the release workflow contract test.
Verification:
- env PATH=/usr/bin:/bin:/usr/sbin:/sbin bash scripts/release-workflows.test.sh
- go run github.com/rhysd/actionlint/cmd/[email protected] on all release workflows
- git diff --check
* docs(release): retain manual SignPath confirmations
Context:
The release workflow now consolidates GitHub environment approval, while SignPath manual confirmations remain an intentional second-party control.
Change:
Clarify workflow comments and the release runbook so the one-approval claim applies only to GitHub. Document that the AMD64 and ARM64 signing requests can each require manual SignPath confirmation.
Verification:
- bash scripts/release-workflows.test.sh
- go run github.com/rhysd/actionlint/cmd/[email protected] on all release workflows
- git diff --check
* fix(agent): honour DeepSeek finish_reason=stop on reasoning-only final answer
DeepSeek thinking mode routinely streams a long reasoning_content and then
finishes with finish_reason="stop" but an empty content block: the model has
signalled completion and its answer lives in the reasoning stream that was
already streamed to the user. The empty-final-answer retry only checked text
content (hasVisibleFinalAnswer), so it misclassified such a turn as "no
answer", overrode the stop signal, and forced another expensive thinking
round (the "still thinking after the task is done" symptom), up to 3 times
- or indefinitely when interleaved with tool calls, since emptyFinalBlocks
only resets on a tool call.
Guard the retry with reasoningOnlyFinishHonoured so a stop + non-empty
reasoning turn is accepted as a final answer instead of retried.
Backward-compatible: scriptedProvider emits no usage chunk, so usage is nil
and the new check returns false, preserving the current retry behaviour
(TestRunRetriesReasoningOnlyFinalAnswer, TestRunStopsAfterRepeatedEmptyFinalAnswers).
Co-Authored-By: Claude <[email protected]>
* fix(agent): scope reasoning-only finish accept to DeepSeek thinking mode
Problem: reasoningOnlyFinishHonoured accepted any reasoning-only
finish_reason="stop" turn from any provider, silently widening the
DeepSeek fix to every backend: local <think>-tag models (ollama/vLLM)
lost the empty-final retry safety net that often recovers a visible
answer on the second attempt, and a gateway that mislabels truncation
as "stop" could have a degenerate turn committed as the final answer.
Root cause: the guard keyed only on wire signals (finish reason plus
accumulated reasoning) with no provider scope, while the symptom being
fixed (#6617) is specific to the DeepSeek thinking protocol.
Fix: gate the accept on provider.RequiresToolCallReasoning — the
existing ToolCallReasoningPolicy capability that is true exactly when
the OpenAI-compat client runs DeepSeek thinking mode (explicit
reasoning_protocol="deepseek", or an official DeepSeek base URL, with
thinking not disabled). Every other provider keeps the unchanged
retry-then-error behaviour.
Verification: new TestRunRetriesReasoningOnlyStopWithoutDeepSeekPolicy
replays the accept test's exact chunk sequence without the capability
and asserts the retry still fires; TestRunAcceptsReasoningOnlyFinalWhenModelStopped
now runs under a DeepSeek-marked provider and still accepts in one
call. go test ./internal/agent/ ./internal/provider/openai/ — ok;
go vet ./internal/agent/ — clean; gofmt — clean.
Co-Authored-By: Claude <[email protected]>
---------
Co-authored-by: Claude <[email protected]>
Co-authored-by: SivanCola <[email protected]>
* feat(desktop): Add Theme Pack V1 engine with theme library / 新增 Theme Pack V1 引擎与主题库
Controlled, non-executable desktop skins: semantic color tokens, density and
corner recipes, and an optional local background image with focus and safe
area. Includes manifest validation, atomic import with replace confirmation,
content-addressed asset serving, live preview with cancel restore, density
consumers, transparent layout under backgrounds, contrast warnings, and
safe-mode fallback to Graphite. / 受控不可执行的桌面皮肤:语义色令牌、密度
与圆角配方、可选本地背景图(焦点与安全区)。包含清单校验、原子导入与覆盖
确认、内容寻址资源服务、可取消的实时预览、密度消费者、背景下布局透明化、
对比度警告与安全模式回退 Graphite。
* feat(desktop): Redesign theme experience with official packs / 主题体验改造与官方主题
Problem:
Appearance settings exposed two competing base-style pickers and applied
theme packs immediately on card click, while activeThemeId could store base
style ids. Official skins and a gallery/overview IA were still missing.
Root cause:
Theme Pack V1 layered packs on top of desktop.theme_style without a unified
state model or select-then-apply UX, so base styles, official themes, and
user packs competed for the same controls.
Fix:
- Bump desktop-theme-state to schema v2: activeThemeId is official/user only;
migrate legacy base-style active ids into desktop.theme_style.
- Add GetThemeExperience, ActivateBaseStyle, DisableThemePack, and
RestoreGraphiteAppearance; reject base ids in ActivateThemePack.
- Embed eight official themes with fixed order, asset routing, and budgets.
- Ship AppearanceOverview + ThemeGallery (select ≠ apply, temp preview,
immersive detail) driven by themeExperience.ts.
- Document the IA and provenance; keep procedural art as placeholders.
Verification:
- cd desktop && go test ./...
- cd desktop/frontend && pnpm typecheck && pnpm test:all && pnpm build
- wails generate module (bindings match, no drift)
* feat(desktop): complete the theme gallery and editor experience
Problem:
The theme gallery still had fragmented preview actions, placeholder flagship art, generic base-style previews, and a minimal custom-theme dialog that could not tune semantic colors or independent scenes.
Root cause:
Theme Pack V1 modeled one background and the gallery treated selection, preview, and persistence as separate interactions without a shared localized editor state.
Fix:
Upgrade the pack contract compatibly to V2 with independent home and workspace scenes, rebuild the gallery around immediate reversible preview and explicit apply, add localized semantic color and scene controls, replace the eight official artworks, and align base-style previews and actions with the settings UI.
Verification:
- cd desktop && go test ./...
- cd desktop/frontend && pnpm test:all
- cd desktop/frontend && pnpm typecheck
- cd desktop/frontend && pnpm check:css
- cd desktop/frontend && pnpm build
- Browser-verified Chinese and English labels, live preview, scene switching, and cancel restoration
* fix(desktop): satisfy the full-module formatting gate
Problem:
The desktop CI job stopped before tests because its repository-wide gofmt check reported main.go and theme_assets.go.
Root cause:
The earlier theme integration left one struct field with manual alignment and one extra trailing blank line; package tests compile these files but do not enforce gofmt byte formatting.
Fix:
Run gofmt on both reported files without changing runtime behavior.
Verification:
- cd desktop && gofmt -l .
- cd desktop && go test ./...
* fix(desktop): clarify theme scene content placement
Problem:
The scene editor exposed a vague content-safe-area label without explaining that it controls foreground text and card placement. The desktop CI lint gate also found four empty or unused Theme Pack leftovers.
Root cause:
The persisted safeArea field was surfaced directly as user-facing copy, while placeholder branches and helpers remained after the Theme Pack integration.
Fix:
Rename the control to Interface content area, add localized guidance and radio semantics, and remove or complete the four lint findings without changing theme behavior.
Verification:
- cd desktop && go test ./...
- cd desktop && go vet ./...
- cd desktop/frontend && pnpm typecheck
- cd desktop/frontend && pnpm check:css
- cd desktop/frontend && pnpm test:all
- cd desktop/frontend && pnpm build
- Theme Pack contract: 195 passed
- Verified the localized control in the running browser UI
* fix(desktop): unify theme confirmation dialogs
Problem:
Theme deletion, replacement imports, and export-rights checks used window.confirm, which surfaced platform-native dialogs that did not match the Reasonix settings UI.
Root cause:
The Theme Pack flows lacked a reusable in-app confirmation primitive, so both the gallery and legacy library called the browser API directly.
Fix:
Add an accessible portal-based Reasonix confirmation dialog with danger styling, safe focus behavior, Escape and backdrop cancellation, and focus restoration. Migrate all six theme confirmation call sites and localize their titles and actions.
Verification:
- pnpm typecheck
- pnpm check:css
- pnpm test:all
- pnpm build
- Browser: delete confirmation stayed in-app, Cancel received focus, Escape restored focus, and the temporary theme was deleted without a native dialog
* fix(desktop): restore theme preview and custom fonts
Problem:
- Custom UI and monospace fonts could no longer be configured from the redesigned appearance page.
- Selecting a user theme updated details without starting the advertised global preview.
Root cause:
- AppearanceOverview discarded both custom font name callbacks and omitted their editors.
- ThemeGallery limited non-immersive previews to the catalog tab.
Fix:
- Restore localized custom font editors and their persistence callbacks.
- Preview every non-immersive theme selection, including user packs.
- Add focused source-contract coverage for both behaviors.
Verification:
- pnpm test:all
- pnpm build
- pnpm check:css
- Browser verification of custom font variables, user-pack preview, and Back restoration
* Remove the Memory v5 execution compiler
Delete the Memory v5 execution compiler end to end: the
internal/memorycompiler package, the agent turn-start compilation and
injection gate, the LLM/heuristic task classifier apparatus, the
/memory-v5 and `reasonix config memory-v5` commands and completions,
the control/boot/config/doctor/event/eventwire wiring, the desktop
settings toggle and Wails binding, compiler metrics buckets, and the
compiler-derived memory suggestions bridge. Update GUIDE (en/zh-CN),
SPEC, the example config, the desktop README, and the site docs.
The feature delivered no provider-visible value on the default observe
path while costing per-turn state I/O and a recurring bug family
(#5316, #5329, #5342, #5468, #5486, #5596): the compact mode replaced
the user turn with an execution contract that over-planned simple
requests into runaway tool loops.
Backward compatibility:
- [agent].memory_compiler is retired via a one-time config migration
(same pattern as redact_tool_output) with a boot notice; old configs
load cleanly either way because unknown TOML keys are ignored.
- Legacy transcripts from releases up to v1.17.x still display
normally: <memory-compiler-execution> blocks keep unwrapping to the
original prompt for previews, titles, history replay, and the
desktop transcript (internal/agent/preview.go,
memoryCompilerDisplay.ts, memoryCitationVisibility.ts), and
persisted compiler memory citations still render.
- The delivery evidence gate keeps its task-vs-chat heuristic, moved
to internal/agent/task_heuristic.go with its regression table.
- On-disk compiler state directories are simply orphaned; nothing
reads or writes them anymore.
Cache-impact: none. The compiler never touched the system prompt,
tool schemas, or provider request serialization; observe mode added no
provider-visible bytes, and compact injection was opt-in. Removing it
cannot shift the stable prefix. Verified by the release cache-hit
guard (all 8 scenarios >= 90% tail average).
Cache-guard: scripts/cache-guard.sh pass (tail_avg 92-95 across
plain-dialogue, long-dialogue, mixed-message-sizes, and tool-loop
scenarios).
System-prompt-review: no system prompt or tool schema text changed;
prompt-construction call sites in internal/agent and internal/boot
only lost the user-turn replacement branch.
* Make retired-key config migrations TOML-multiline-safe; drop unused metric buckets
Address review findings on #6615:
1. The retired-key config migrations (memory_compiler, redact_tool_output,
agent step limits, MCP tier) scanned line-by-line without understanding
TOML multiline strings, so a `memory_compiler = ...` example line quoted
inside a multiline `system_prompt = """..."""` was deleted from the
prompt and the config rewritten. All four strippers now share
stripTOMLKeyLines, which tracks multiline basic/literal string state
(honouring single-line strings and comments so their quote delimiters
cannot open a multiline state) and never treats lines inside a multiline
string as section headers or key assignments. The scanner is
deliberately conservative: on any ambiguity it keeps lines rather than
deleting them, and a dropped line must itself leave the scanner outside
a multiline string. Regression tests cover the reported reproduction
(multiline system_prompt survives byte-for-byte), literal strings,
section headers inside strings, single-line triple-quoted values,
comments containing triple quotes, and the real-key-next-to-string case,
plus a full MigrateLegacyMemoryCompilerForRoot test.
2. Remove tokenSizeBucket and memorySizeBucket from desktop/metrics_app.go;
they lost their only caller with observeMemoryCompilerStats and failed
the desktop golangci-lint (unused) check.
Cache-impact: none; config-file migration and desktop metrics helpers are
not provider-visible surfaces.
Cache-guard: n/a beyond the base PR's passing run; no prompt construction,
tool schema, or serialization path is touched by this commit.
System-prompt-review: the migration now provably preserves user
system_prompt content (multiline regression tests); no prompt text changed.
* fix(provider): bound non-OK response body reads in the retry loop
A 502/524 gateway under load can send headers and then stall the body on a
half-open connection. http.Client deliberately has no Timeout (streams), and
ResponseHeaderTimeout no longer applies once headers arrive, so the retry
loop's error-body io.ReadAll/io.Copy could block indefinitely with no retry
notification and no user-visible progress — the turn looked frozen until the
process was killed (#6607, #6346, #5760).
readErrorBody arms a hard deadline that closes the body to unblock the read;
context cancellation already aborts it via the transport. The regression test
drives a stalled 502 body through SendWithRetry and fails via watchdog if the
loop wedges (mutation-verified).
* fix(agent): salvage event-log tail bytes before repair truncates them
repairSessionEventLogTail's os.Truncate was the only place that permanently
discards persisted transcript bytes. A torn tail is usually one partial
record, but replay also stops at a buried undecodable or out-of-order record
(the dual-writer interleave shape) — then every intact turn past it was
silently lost, surfacing as 'reopened session lost messages' (#6607, #5315).
The discarded range is now appended to a .events.jsonl.damaged salvage
sidecar (one-line JSON incident header + raw bytes) before truncation. The
sidecar name must not end in .jsonl: older binaries scanning a shared session
directory would classify it as a primary transcript. It is registered in
SessionSidecarFiles so every delete surface removes it. Preservation is
best-effort — a full disk both tears tails and fails salvage — and never
blocks the repair itself.
* perf(agent): skip snapshot saves when the transcript is provably unchanged
Desktop switch/close/prune paths snapshot defensively on every navigation.
Each snapshot paid a full transcript serialize + digest + disk probe just to
discover there was nothing to write — on large sessions that is the seconds
of UI freeze reported as switch lag (#6607, #5337, #5185; single-surface
activation pays it once per hidden tab, twice per pruned runtime).
save() now short-circuits snapshot-mode saves when in-memory bookkeeping
proves a no-op: same path, same version, same rewriteVersion, ledger revision
known, no pending load-time repair or log damage. The check runs under the
save locks, not before them, so a saver that waited on a concurrent writer
still re-evaluates against the state it must persist (locked in by
TestSaveSnapshotCapturesContentUnderSaveLock).
Only save-verified baselines arm the fast path: a baseline adopted at load
pairs the disk transcript with whatever the meta sidecar said, which lags the
transcript after an interrupted save — the first save after a load must run
in full and heal a stale ledger (TestSameContentSaveHealsStaleLedgerDigest).
* fix(desktop): drop stale topic activations instead of applying them
activateTopic applied its result unconditionally after awaiting the backend:
when a newer navigation (another topic click via a different path, a tab
switch, session restore) completed first, the stale completion flipped the
visible tab away from the user's last click AND ran the single-surface prune
that deletes every other tab's cached state — blanking the transcript the
user was looking at (#6607, #5337 'switch shows an empty session').
The App-level navigation queue serializes queued topic clicks, but its
latest() guard only covers App-level effects; the setActiveTabId + state
prune inside activateTopic were unguarded. Anchor each activation to the
existing activeNavigationSeqRef epoch (the same mechanism rewind/fork already
use) and return without applying when a newer navigation superseded it.
Mutation-verified: disabling the guard fails both stale-apply assertions.
* fix(privacy): scrub and trash the damaged-salvage sidecar with its session
Review of #6613 found two lifecycle gaps for the new .events.jsonl.damaged
salvage sidecar, both privacy-relevant since it preserves raw transcript
bytes:
- doctor redact did not recognize the suffix: RedactSessions reported success
while secrets in the salvaged tail survived. The bytes are undecodable by
definition, so no format-aware masking can prove them clean; following the
existing torn-tail precedent (compacted away regardless of content), the
scrub now deletes the sidecar outright, honors dry-run, and skips leased
sessions like every other artifact.
- the desktop trash flow uses its own explicit artifact list, so deleting a
session left the sidecar in the live directory and purging the trash never
removed it. The sidecar now moves with the other transcript artifacts
through delete, restore, and purge.
* fix(agent): re-arm the snapshot fast path after a repair is persisted
Review of #6613 found that normalizedDirty and eventLogDamaged were never
cleared on the live session after a successful save persisted the repair, so
a session that was repaired once kept paying the full serialize + digest on
every defensive snapshot until restart — the switch-lag fix was incomplete
for exactly the sessions that needed it most.
Clear the flags (and the obsolete rawMessages capture) when a save-verified
baseline lands: the write included the normalization repair and healed the
on-disk log. Nothing reads the live session's copies after a save —
checkSnapshotWrite re-loads the on-disk state and consults that object's
flags, not these.
* fix(desktop): invalidate in-flight activations when a navigation is queued
Review of #6613 found the last-click-wins guard did not cover the production
navigation queue: App.enqueueNavigation serializes clicks, so a click made
while another request runs only becomes a pending queue entry — it does not
run activateTopic and therefore never advances the controller's navigation
epoch. The running stale activation then passed the guard, flipped the
visible tab, and pruned the newer surface's cached state.
useController now exposes noteNavigationIntent (the same epoch bump the
guard checks) and App.enqueueNavigation calls it at enqueue time, so any
queued click immediately invalidates the running activation's destructive
apply. The regression test drives the REAL enqueueNavigationRequest queue,
holds the first activation mid-flight, queues a second, and asserts the
stale completion neither flips the tab nor deletes cached state; a source
assertion locks the App.tsx wiring itself. Mutation-verified both ways.
* fix(doctor): include the damaged-salvage sidecar in session bundles
Proactive sweep after the #6613 review round: session bundles exist to debug
persistence incidents, and the .events.jsonl.damaged sidecar is the primary
evidence for truncation incidents (torn tails, dual-writer interleaves) —
but sessionBundleArtifacts, another explicit artifact list, did not include
it. Bundles copy artifact contents verbatim already (only paths/refs are
redacted), so shipping the sidecar adds no new sensitivity class.
* fix(agent): migrate the damaged-salvage sidecar on overlong-name renames
Review follow-up on #6613: migrateSessionSidecars is yet another explicit
sidecar list, and it did not include SessionEventLogDamaged. When
reconcileOverlongSessionFilenames renames a transcript to a bounded stem,
the salvage sidecar stayed under the retired name — an orphan file holding
raw session bytes that no delete, trash, redact, or bundle flow could reach
(they all derive paths from the live transcript name).
Regression: TestReconcileOverlongMigratesDiagnosticSidecars now seeds the
sidecar and asserts it migrates with the rename and leaves nothing under
the retired stem (mutation-verified).
* fix(mcp): preserve stateful connections and simplify trust
Problem:
MCP identity drift invalidated existing trust receipts, while per-call writer processes repeatedly prompted stateful servers and added startup latency.
Root cause:
Host-local isolation and config-source details leaked into stable identity matching, and stdio writers used a separate one-shot process path that discarded server state.
Fix:
Migrate compatible legacy receipts, keep identity stable across host-policy changes, apply source-aware launch authorization, and reuse one persistent stdio process for reads and writes without an extra writer-isolation setting.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- cd desktop && env -u DEEPSEEK_API_KEY go test ./...
- cd desktop/frontend && pnpm test:typecheck
- cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- ./scripts/cache-guard.sh
- git diff --check
* test(mcp): use real sandbox paths in migration coverage
Problem:
The Ubuntu CI runner rejected the MCP migration test before startup because bubblewrap could not bind a hard-coded /workspace path that did not exist.
Root cause:
The test used illustrative absolute paths for enforced sandbox roots instead of creating filesystem fixtures.
Fix:
Create real temporary workspace and forbidden-read roots, then use them consistently for the trust manager and sandbox policies.
Verification:
- go test ./internal/plugin -run TestLegacyPolicyCoupledIdentityAndWorkspaceSourceMigrate -count=1
- go test ./internal/plugin -count=1
- git diff --check
* fix(mcp): complete add-and-use trust flow
Problem:
- Windows trust-state writes could fail during lock-file delete/create races.
- /mcp add did not receive user-authorized policy until the next restart.
- Project MCPs kept showing launch authorization after connecting and offered two scopes for a one-time launch gate.
Root cause:
- Windows can report access or sharing violations instead of file-exists during exclusive-create lock handoff.
- The Controller add path connected an entry before marking its saved user-config provenance.
- Desktop exposed source policy as current blocked state and reused the generic trust-scope modal.
Fix:
- Retry transient Windows lock handoff errors within the existing deadline.
- Mark explicit Controller adds as user-configured before live connection.
- Show project launch authorization only while blocked and collapse it to one durable workspace action.
- Document the persistent writer-sandbox tradeoff accurately.
Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- cd desktop && env -u DEEPSEEK_API_KEY go test ./...
- cd desktop/frontend && pnpm test:typecheck
- cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- go test ./internal/mcptrust -count=20
- GOOS=windows GOARCH=amd64 go test ./internal/mcptrust -c
- ./scripts/cache-guard.sh
- ./scripts/check-cache-impact.sh
- git diff --check
* fix(agent): resolve on-demand MCP approval through the shared spec policy
The delivery-mode on-demand proxy re-derived the approval mode from raw
Spec fields and missed ImplicitApproval, so a user-configured server that
runs with direct approval on the normal MCP path prompted again under the
global Ask posture. Export plugin.Spec.ToolApprovalMode as the single
resolution and route every MCPApprovalPolicy implementer through it.
* fix(desktop): keep a revoke entry for authorized project MCP servers
Hiding the whole trust panel once a launch-gated server connects also hid
the only Revoke trust button, leaving persistent project authorization
irrevocable from the desktop UI. Split the static launch-gate governance
flag from the blocked-now flag and show a calm revoke row for servers
whose workspace/session grant is currently established. Revoke stays
hidden for user-config and official sources where auto-trust would
silently re-trust on the next evaluation.
* feat(cli): polish composer and responsive status UI
* fix(cli): remove obsolete status helpers
* chore: credit status footer co-contributor
Attribution:
Credit @billycat for the full-data TUI status footer restoration and height-accounting coverage contributed in #6587.
Relationship:
This PR carries that behavior forward in the redesigned responsive footer. This attribution-only commit does not change the runtime tree.
Verification:
Confirmed this commit has the same tree as its parent.
Co-authored-by: billycat <[email protected]>
* polish(cli): refine receipt and themed footer
Problem:
The wide footer overemphasized per-turn metadata, separated effort from its model context, and used faint or hard-coded colours that did not preserve the same hierarchy across dark and light terminals.
Fix:
Use a single receipt boundary with softer separators, group model/effort/work together, add a theme-specific subtle contrast token, and render explicit effort through each theme's semantic info colour. Keep dark, light, and no-colour geometry identical.
Verification:
- go test ./internal/cli -count=1
- DEEPSEEK_API_KEY= go test ./... -count=1
- git diff --check
* polish(cli): tint composer borders with theme accent
Problem: The composer top and bottom borders stayed neutral, so the input area did not reflect the active CLI theme.
Root cause: refreshCLIStyles bound the composer border foreground to the neutral border token.
Fix: Bind both composer borders to the active accent and cover every dark and light theme style plus NO_COLOR.
Verification: Focused composer theme test and the full internal/cli suite pass with an isolated user home.
* fix(cli): localize persistent footer labels
Problem: Chinese and Traditional Chinese sessions still showed English metric labels in the persistent footer.
Root cause: The new footer embedded MODEL, EFFORT, WORK, CACHE, CTX, COMPACT, JOBS, and BAL as literals outside the i18n catalog.
Fix: Add localized footer labels, use them at every metric call site, and calculate model width from the active label.
Verification: i18n catalog tests and the full internal/cli suite pass with an isolated user home.
* fix(cli): spell out footer shortcuts
Problem: The persistent footer used terminal shorthand for Shift+Tab and Ctrl+Y, which was difficult to recognize at a glance.
Root cause: The compact locale strings preferred symbolic key notation without a responsive fallback for the extra width of readable names.
Fix: Spell out both shortcuts in every locale, wrap semantic groups on medium terminals, and hide optional shortcut help only when a complete key-name group cannot fit.
Verification: The i18n catalog and full internal/cli test suites pass with an isolated user home.
* fix(cli): localize work mode values
Problem:
Chinese CLI sessions showed an English work-mode value such as '模式 balanced', unlike the localized desktop UI.
Root cause:
The CLI used one helper both for stable command identifiers and user-facing profile labels, so English command names leaked into localized status and notices.
Fix:
Separate stable work-mode names from localized display labels, add English, Simplified Chinese, and Traditional Chinese catalog entries, and keep slash-command completion values unchanged.
Verification:
- go test ./internal/i18n -count=1
- go test ./internal/cli -run 'Test(StatusFooter|WorkMode|RuntimeProfileDisplay|RenderWorkModes)' -count=1
- git diff --check
* fix(cli): use platform image paste shortcut in tests
Problem: The image-paste selection regression test always sent Ctrl+V, so the Windows CI job could not enter its Alt+V image-paste path.
Root cause: The test ignored the platform mapping already enforced by imagePasteShortcut.
Fix: Select Ctrl+V on macOS/Linux and Alt+V on Windows, and make the test name and diagnostics platform-neutral.
Verification: The isolated internal/config, internal/i18n, and internal/cli suites passed; the Windows/amd64 internal/cli test binary also cross-compiled successfully.
* docs(cli): document responsive composer controls
Documentation: Explain footer states and telemetry, theme-aware composer behavior, platform-specific text and image paste, and the stable mode shortcuts in the English and Chinese CLI guides.
Website: Mirror the operation and shortcut guidance on the public docs page and refresh the CLI homepage summary.
Changelog: Record the responsive composer, localized footer, clipboard split, and theme behavior under Unreleased.
Verification: The site test suite and Astro static build passed, and git diff --check reported no whitespace errors.
---------
Co-authored-by: billycat <[email protected]>
Problem:
MCP identity drift was blocked before startup, but the server list still offered Retry, which could only repeat the same preflight failure.
Root cause:
Recorded MCP failures kept only a display string, so the desktop could not distinguish re-verification requirements from ordinary connection failures.
Fix:
Preserve identity-drift failures as structured state, surface Reverify directly in the server row, keep OAuth and ordinary retry precedence, and reconnect failed servers after explicit trust confirmation.
Verification:
- go test ./internal/plugin
- go test ./... (with the live DeepSeek probe disabled)
- cd desktop && go test ./...
- pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- pnpm typecheck
- pnpm test:typecheck
- pnpm check:css
* fix(desktop): persist workspace panel and harden Windows openers
Remember the workspace dock open/collapsed preference across restarts
(regression from always-default-open), discover custom VS Code installs via
App Paths, improve Windows Terminal icons, and launch console shells through
detached start so PowerShell/CMD no longer flash-exit.
* fix(desktop): shell-free Windows console open and real WT icons
Address PR #6588 review:
- Launch PowerShell/CMD via ShellExecuteW with lpDirectory instead of
cmd /c start, so workspace paths with shell metacharacters cannot be
re-parsed and start title quoting cannot mis-bind.
- Look up Windows Terminal icons under Program Files\WindowsApps package
layout first; App Execution Alias dirs only expose wt.exe stubs.
- Add pure, injectable plan/candidate tests that cover special-character
workdirs without starting a real console process.
* fix(desktop): prefer renderable WT icon over zero-byte alias
When Program Files\WindowsApps cannot be enumerated, skip zero-byte wt.exe
App Execution Aliases and use a PowerShell (or other non-zero) icon fallback
so the menu does not show a blank glyph. Add deterministic pick() assertions.
* docs(release): prepare v1.17.14 notes
Generate a bilingual, product-focused draft from merged pull request metadata. The reviewed catalog will drive the website, desktop app, and GitHub releases.
* fix(release): persist guide link revisions
Problem: Generated release-note guide URLs used the transient HEAD ref, which could stop resolving after the preparation branch was deleted.
Root cause: The generator interpolated the git revision expression directly into public GitHub URLs.
Fix: Resolve the source revision to an immutable commit SHA and update the v1.17.14 guide links to the reviewed candidate commit.
Verification: node --check scripts/generate-release-notes.mjs; node scripts/release-notes.mjs validate; node --test scripts/release-notes.test.mjs; rendered-link and privacy scan; git diff --check
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Problem: The release-notes preparation workflow failed after generating a new version because its test required 1.17.13 to remain the first catalog entry.
Root cause: The test duplicated mutable release data as a hard-coded constant even though loadCatalog already validates descending version order.
Fix: Remove the stale version assertion while retaining schema, ordering, and uniqueness validation through the catalog loader.
Verification: node scripts/release-notes.mjs validate; node --test scripts/release-notes.test.mjs; git diff --check
Problem:
Release notes were assembled independently from commit-oriented GitHub output, leaving the website, desktop updater, and CLI/Desktop releases without one reviewed product narrative.
Root cause:
There was no structured bilingual release catalog or review workflow shared by the site, desktop UI, and tag publishers.
Fix:
Add a validated release catalog and DeepSeek-assisted draft workflow, publish responsive website changelog pages with shared navigation, link Desktop update surfaces to the web history, and render the reviewed notes into CLI and Desktop GitHub releases.
Verification:
- node scripts/release-notes.mjs validate
- node --test scripts/release-notes.test.mjs
- npm test and Astro build in site/
- pnpm run build in desktop/frontend/
- actionlint for all affected workflows
- git diff --check
Cache-impact: none - release metadata, website UI, desktop links, and publication workflows do not change provider-visible prompts, tool schemas, request serialization, or ordering.
Cache-guard: existing scripts/cache-guard.sh remains applicable; no cache-sensitive path changed.
* fix(desktop): show MCP npm connection endpoints
Problem:
MCP npm launcher failures such as ECONNREFUSED were collapsed to the error code, hiding the registry and refused proxy endpoint users need to repair their configuration.
Root cause:
The desktop summary extracted only the npm code and discarded network context already present in stderr. The same errors were also grouped as generic failures.
Fix:
Preserve the credential-free registry host and connection endpoint in summaries, classify common npm network codes correctly, and cover private-registry redaction.
Verification:
- pnpm --dir desktop/frontend test:typecheck
- pnpm --dir desktop/frontend test
- pnpm --dir desktop/frontend build
- real npx filesystem MCP failure through an unreachable proxy
- real npx filesystem MCP startup through the official registry
* fix(desktop): parse legacy npm network errors
Problem:
Reasonix supports Node 18 environments whose npm releases emit network failures with the legacy npm ERR! prefix, leaving the registry and refused endpoint hidden.
Root cause:
The desktop MCP error summary recognized only the newer npm error code prefix.
Fix:
Accept both npm error and npm ERR! prefixes and add a regression based on the real npm 9 failure format.
Verification:
- pnpm --dir desktop/frontend exec tsx src/__tests__/capabilities-panel-actions.test.ts
- pnpm --dir desktop/frontend test:typecheck
- pnpm --dir desktop/frontend test
- pnpm --dir desktop/frontend build
- real npm 9.9.4 npx failure through an unreachable proxy
Problem:
On Windows, cmd.exe does not expand POSIX-style plugin-root variables or provide
`sh`, so plugin hooks received a literal ${REASONIX_PLUGIN_ROOT} path and explicit
`sh -c` / `bash -c` hooks failed before their scripts could run. Localized cmd.exe
errors were also surfaced to the UI as mojibake.
Root cause:
- expandPluginRoot only recognized ${CLAUDE_PLUGIN_ROOT}/$CLAUDE_PLUGIN_ROOT, and
env expansion ran after host vars were set, so host-provided paths could be
accidentally expanded.
- spawnCommand always went through cmd.exe on Windows, which cannot find a Git Bash
installed outside its PATH.
- Hook stdout/stderr were surfaced as raw bytes, so legacy code-page (GB18030)
output reached the renderer as mojibake.
Fix:
- Expand ${CLAUDE_PLUGIN_ROOT}/${REASONIX_PLUGIN_ROOT} plus their unbraced $NAME and
Windows %NAME% spellings via a single-pass scanner that never re-expands the
resolved root; expand plugin env before writing host vars.
- Route explicit bare `sh -c` / `bash -c` hooks on Windows through the shared
hardened Git Bash discovery, preserve explicit interpreter paths, and return an
actionable error when no usable Bash is found.
- Normalize legacy code-page hook output to valid UTF-8, preserving truncated
multibyte prefixes and leaving UTF-8-native output byte-for-byte.
Verification:
- go test ./internal/hook ./internal/pluginpkg ./internal/config
- go test -race ./internal/hook
- go vet ./internal/hook ./internal/pluginpkg
- GOOS=windows GOARCH=amd64 go test -c ./internal/hook
- CI: all checks green (cache-impact, desktop-windows, test (windows-latest), race,
CodeQL, govulncheck)
Cache-impact: none - only local plugin hook spawning and output decoding change;
provider-visible prompts, tool schemas, and ordering are unchanged.
Problem: Manual-only desktop updates opened the generic install anchor with the npm pane selected, forcing users to find the desktop downloads themselves.
Root cause: The updater manifest only linked to #start, while the site activated the desktop pane and highlighted the detected OS only for an in-page click.
Fix: Add a desktop-specific install deep link, activate and scroll to its pane on page load, preserve plain #start behavior, and keep updater manifests and release checks aligned.
Verification: npm --prefix site test; npm --prefix site run build; cd desktop && go test ./cmd/sign .; pnpm --dir desktop/frontend typecheck; pnpm --dir desktop/frontend check:css; local browser deep-link verification.
The construction boundary is the shared pairing NewReadOnlyAgent /
RunReadOnlySubAgentWithSession; the registry filter removes host-mutating
tools and authority-less MCP readers while receipt-matched trusted readers
may still start on demand; and the Plan-mode MCP block is a hard phase
block that approval cannot release — it lifts only when Plan exits. GUIDE
in both languages, SPEC, the website, and MIGRATING now match the enforced
semantics.
Reader-authorized calls now carry an immutable execution intent into
dispatch: the agent pins the reader basis plus the capability fingerprint
in the call context, and remoteTool.ExecuteWithImages re-verifies both
linearizably under the owning client's toolsMu — the same lock every trust
mutation (catalog revocation, live security reconciliation) takes. A call
approved as a non-destructive reader can therefore never promote itself
into the one-shot writer lane or reach tools/call after a concurrent
revocation or reclassification; it returns an actionable re-verify error
with zero extra process starts and zero tool calls, while writer-approved
calls keep their lane unchanged. Covered by a helper-counter regression
that flips trust between authorization and dispatch and asserts both
counters stay flat, plus a stale-fingerprint refusal.
Strict read-only execution now also requires a positive trust authority
for MCP-backed tools: the no-TrustManager compatibility paths (server
hints, legacy read-only config) keep their historical classification for
ordinary sessions but can no longer admit tools into a strict child's
registry or its on-demand host-startup gate. A missing trust store fails
closed with zero starts and zero executions.
NewReadOnlyAgent joins RunReadOnlySubAgentWithSession as the shared
construction pairing (permanent ReadOnlyExecution flag plus the final
registry filter), and the two-model planner now builds through it instead
of assembling the pieces by hand.
Every strict read-only child shares one construction boundary
(RunReadOnlySubAgentWithSession); the guides now list the entrances —
read_only_task, read-only parallel_tasks, read_only_skill, CLI review,
desktop preview/review subagents, and the two-model planner — and state
the boundary properties: final registry filtering of writers/destructive/
untrusted-reader/host-mutating tools, post-resolution use_capability
re-checks, receipt+identity+cached-fingerprint-gated on-demand reader
startup with no trust creation or upgrades, zero executions on live
drift, no auto_review privilege elevation, and fail-closed local prompts.
Both languages also state how this layer is stricter than the main Plan
workflow.
The website still claimed MCP writers use the normal Ask/Auto/YOLO path while
planning; it now describes the three execution layers — ordinary sessions,
the main Plan workflow with MCP writer/destructive targets and untrusted
readers blocked until approval, and strict read-only children that never
expose writers. MIGRATING.md no longer says destructive calls fail closed on
an unavailable reviewer (they never consult one — every call takes a fresh
human approval) and its receipt-migration promise now matches the pre-start
gate behavior. The PR description's auto_review fallback wording was updated
to the fresh-human contract alongside this commit.
The pre-start identity gate returned before initialize/tools-list while the
legacy URL migration only ran inside the post-handshake evaluation, so eager
and cache-miss servers with a pre-rollout receipt were blocked with a manual
re-verification instead of migrating. start() now upgrades a receipt whose
identity exactly matches this spec's legacy URL fingerprint before treating
the identity as changed; no capabilities exist at that point, so the drift
comparison is skipped there (the tool snapshot is untouched and the
post-handshake evaluation still runs the full drift check). Covered by a real
StartAll regression over the Streamable HTTP harness: legacy receipt starts
and migrates, credential rotation keeps starting, and a moved resource scope
still blocks before any network handshake.
The credential query classifier now recognizes whole families by normalized
suffix (*token, *secret, *password/*passwd, *apikey, *signature) plus an
explicit key/auth family (key, access_key, secret_key, private_key, auth_key,
app_key, client_key, subscription_key, shared_key, bearer, credentials), so
auth_token, refresh_token, id_token, and x-api-key values stay out of
identity/cache hashes while workspace, tenant, region, and similar resource
parameters remain identity-bearing.
Plan is documented as a plan-first workflow, not an all-tools read-only
mode: built-in writers keep Permissions/Sandbox while installed MCP and
proxy-resolved MCP writer/destructive targets plus untrusted readers stay
blocked until plan approval, returning to their normal approval flow when
Plan exits. The "every writer follows Ask/Auto/YOLO while planning"
claims are removed from GUIDE/SPEC/MIGRATING.
Destructive MCP calls document as always requiring a fresh human
approval that no reviewer, Auto/YOLO posture, or session grant can
answer. auto_review documents its reviewer-entry conditions (prompt,
writes hits, and auto calls the global posture would Ask about), the
finality of a successful verdict, and the fresh-human degradation for
missing or failed reviewers. MIGRATING.md gains the credential-aware MCP
URL identity migration window.
A successful auto_review verdict — allow or deny — stays final. When the
configured reviewer is missing, times out, errors, or returns no verdict,
the call no longer falls into the ordinary approval flow: it now requires
a strict fresh human decision that Auto/YOLO, the approved-plan window,
and session grants cannot answer, and the decision never mints a session
grant of its own. Non-interactive gates keep failing closed before any
approver is consulted, and approval_mode=auto still inherits the global
posture without consulting the reviewer when that posture already allows
the call.
guardian.ReviewVerdict exposes the authentic-verdict/failure distinction
(transport errors, timeouts, unparseable assessments) with unchanged
circuit-breaker bookkeeping, while Review keeps the legacy fold-into-deny
contract for the empty-reviewer routing.
- normalizeIdentityURL redacts URL userinfo and credential query values
(token, access_token, api_key, authorization, auth, password, passwd,
secret, client_secret, credential, signature, sig — matched
case-insensitively after removing separators) while keeping resource
scope parameters such as workspace and tenant; query keys sort stably
and fragments drop. Only the placeholder structure survives, so
credential rotation keeps trust while a scope change still re-verifies.
Network requests keep using the raw configured URL.
- The previous normalization survives as a read-only legacy calculator:
managerEvaluate migrates receipts and LoadCachedSchemaForSpec rewrites
schema-cache entries in place when nothing else drifted, so the rollout
and credential rotation never force a re-trust or a re-handshake.
Removal window: two minor releases (docs/MIGRATING.md).
- managerEvaluate is the single evaluation entry for every executing
path — live handshake, lazy cache preflight, cached-tool trust, and the
trust refresh — and routes official servers through EvaluateOfficial
with the current signed reader allowlist.
- Git launcher pins accept exactly complete 40- or 64-hex object names;
intermediate lengths resolve through git ls-remote like any ref. uvx
pins reject PEP 440 wildcard versions while keeping exact prerelease,
post, dev, epoch, and local pins, and the official catalog validation
shares both predicates with mutable launcher locks.
- slug() suffixes Windows-reserved device stems (CON, PRN, AUX, NUL,
COM1-9, LPT1-9) so cache JSON, stats files, and MCP state directories
never name a DOS device; ordinary safe names stay byte-identical and
old reserved-name paths are not migrated (they never worked on
Windows).
- EvaluateOfficial: official catalog servers evaluate reader authority
against the current signed allowlist instead of the receipt's historical
trusted_reader flags. A reader removed from the catalog loses authority
immediately; a newly listed reader is granted only while its
already-verified capability snapshot still matches and it remains a
non-destructive reader; a receipt issued by a different catalog entry
grants nothing. The receipt keeps identity and drift snapshots only, and
its persisted format is unchanged.
- MigrateIdentityFingerprint: a receipt whose identity is exactly the
legacy URL fingerprint upgrades atomically to the current one when no
live capability drifted. Only digests are compared; URLs and credential
values are never read or persisted here.
- stripDisplayFields: dependentRequired and legacy dependencies are
containers keyed by user property names — constraint keys named like
annotations (title, description, examples) survive, array and schema
value forms are both handled, and schema-form values still shed their
display-only annotations.
- Receipt identity: Source leaves the receipt key, an explicit user
receipt replaces (and can never be replaced by) a legacy import, and
load-time normalization dedupes older duplicated states — explicit user
over legacy import, then latest verification — without a store version
bump. Session and official global scopes keep their own slots.
TestWindowsSandboxAccessSIDScopes released the current-user SID with
FreeSid, but currentProcessUserSID returns a Go-allocated copy
(SID.Copy), so FreeSid corrupted the Windows process heap and the
winsandbox test binary exited with STATUS_HEAP_CORRUPTION (0xc0000374).
Go-allocated SIDs are garbage collected; drop the FreeSid.
Temporary image-loader grants were removed with one icacls child
process per SID while holding the global ACL-mutation mutex, adding
multi-second serialized teardown to every sandboxed start and pushing
concurrent MCP reader startups past their deadlines on CI. Revoke the
recorded loader SIDs through one SetEntriesInAcl(REVOKE_ACCESS) update
instead, both after CreateProcess and in the crash-residue sweep.
Sweep of the remaining trust-boundary gaps around the reviewed MCP
security fixes, each with a focused regression test:
- Official catalog servers: reverify the installed package digest
before any process or network startup, bind the catalog transport to
the configured transport, and require immutable launcher locators
(exact npm/PyPI versions or full Git commits). Desktop and CLI
inspect/revoke flows resolve the same official trust metadata as
boot, so they can no longer downgrade an official server to a custom
spec, and a manual catalog refresh applies fresh revocations to every
live shared host immediately.
- Identity fingerprints: environment key case folds only on Windows
(env names are case-sensitive on Unix), forbid_read roots join the
identity, display-field stripping no longer erases user-defined
schema properties named like annotations, and receipts never match
across different config sources.
- Schema cache: SpecFingerprint binds the server name and normalized
URL, sanitized slugs get a content hash so confusable names cannot
share cache files or MCP state directories, and cache writes go
through the shared atomic-write helper so Windows replaces work under
concurrent readers.
- Tool listing: reject empty or duplicate raw tool names before they
can alias each other's approvals, and give one-shot writer lanes the
same bounded settle window as the reader lane.
- HTTP transport: configured credential headers are no longer resent
when a server redirects to a different origin.
- Concurrency: remoteTool security fields are read through a snapshot
taken under the owning client's toolsMu; the trust preflight builds
its cached snapshot outside that lock (a snapshot inside the critical
section deadlocked SetSpecTrust).
Lowercase-safe cache slugs keep their filenames; mixed-case or
punctuated server names re-fetch their schema cache once after upgrade.
Portable tree digests now exclude POSIX modes and disable autocrlf so
signed catalog packages verify identically on Windows checkouts.
TestUnavailableMessageIsActionable still required the config-edit
remediation on every platform, but Windows now reports the backend as
retired with the effective mode fixed to off, and pointing users at a
Settings edit the platform ignores would be misleading. Assert the
platform-appropriate actionable content instead.
The English SandboxEscapeRuntimeReason was reworded to "The OS sandbox
could not start this command", which no longer matched the frontend
localizeApprovalReason predicates, so a Chinese desktop UI paired with an
English backend would show the raw English reason. Keep the old matchers
for older CLI backends and add the new phrase, with a zh-CN regression
assertion.
The pull_request events for c8723be96 and the first retrigger commit
were dropped while the repo's ci.yml/codeql.yml scheduling was stuck
(their zombie queued runs from May return HTTP 500 on cancel). Actions
is scheduling new events again, so fire one more synchronize event.
GitHub Actions dropped the CI and CodeQL run creation for c8723be96
(only the label and cache-impact workflows were scheduled, and a PR
close/reopen did not recreate them). Empty commit to re-fire the
pull_request synchronize event.
The first level-aware pass required a phase and its active sub-step to
both be in_progress, contradicting the provider-visible todo_write
contract (exactly one in_progress item). The active sub-step is now the
only in_progress item: its phase stays pending while sub-steps complete
in order, then the phase becomes the single in_progress item and signs
off last, so the tool description and schema stay true byte-for-byte.
todo_write now rejects a level-1 sub-step with no phase above it, and
AdvanceSerialTodo advances legacy orphan sub-steps as standalone steps
instead of stalling the list with no current item.
- Desktop shutdown resets the crash-loop counter and blesses a
probationary update only after the window reached domReady, so cleanly
quitting a build that boots but never paints keeps the incomplete
startup record and the rollback backups.
- Serialize cross-process pending-update transitions (prepare, rollback,
commit, cancel) with a repair-state file lock: two concurrently
launched Guards could otherwise fight over restoreReleaseUnit's fixed
staging paths and re-install the new release over a completed rollback.
- Safe Mode now also drops host-supplied ACP MCP servers and skips the
Memory v5 compiler, matching the documented recovery boundary.
- Pin the normal-mode tool surface alongside the existing Safe Mode pin.
Two-level task lists now validate as one current chain: the active
level-0 phase stays in_progress while its level-1 sub-steps complete in
order, and the phase itself signs off last. complete_step refuses a
phase whose sub-steps are unfinished, todo_write continuity protects
every in_progress chain item, and the shared evidence.AdvanceSerialTodo
helper drives live host advance, session rebuild, and desktop history
replay so all three stay chain-valid. Flat lists keep their existing
rules and messages.
The soft wording loses the first visible-reasoning segment on Chinese
prompts that embed English logs or code, and the first segment anchors
the whole turn once providers round-trip prior reasoning through
tool-call loops. Sampled against deepseek-v4-pro with an agent-style
English system prompt and tool schemas, the soft block held 6/9 first
segments (0/3 on the English-log shape) while the imperative block held
9/9, with no regression on deepseek-v4-flash (24/24 on both arms), and
it partially recovers mid-turn drift (0/4 -> 2/4).
Document the anchoring and drift boundary in both REASONING_LANGUAGE
docs and pin the imperative anchors with a focused test. Tag structure,
injection position, and per-turn transience are unchanged, so the
provider-visible stable prefix and old session histories are unaffected.
Absorb the two improvements from PR #6413 (#3271):
- When the user turns plan mode off and then denies the pending plan
approval, suppress auto-plan for the next turn so it cannot immediately
re-enter the mode the user just left. The suppression is one-shot.
- Make gateApprover check preApproved explicitly before any guardian
review or prompt, so the YOLO / approved-plan-window / session-grant
short-circuit is stated at the approval boundary instead of relying on
the prompt path to duplicate it.
Add regression coverage for both: the exit-then-deny turn sequence, and a
session grant answering an ordinary approval with zero guardian reviews.
Co-authored-by: jiangrenhang <[email protected]>
- Editing an existing server now materializes explicit clear values for
non-secret fields the user removed from the form or advanced JSON;
absent env/headers still preserve stored secrets.
- Reject null per-tool timeout values instead of silently clearing every
stored timeout through an empty map.
- Validate MCP approval mode and reviewer fields at install plan time,
before the server subprocess is ever connected.
- Cover UpdateMCPServer preserve-vs-clear semantics, align the destructive
approval test fixture with the real i18n strings, and document the new
plugin approval fields in reasonix.example.toml.
Restore the plan-mode routing that lets installed MCP writers fall through
to the ordinary Ask/Auto/YOLO permission posture instead of a hard
planning-stage block, per maintainer decision. Built-in writers remain
Plan-blocked, PlanSafetyUnsafe remains a hard stop, and third-party
readOnlyHint still grants no Plan/planner/read-only-research trust.
Align every surface that described this boundary: the public site page no
longer claims hinted readers join plan mode, the GUIDE mode table names the
MCP-writer exception, and the GUIDE gains explicit notes on the writes-mode
classification trust boundary, legacy Guardian routing under prompt/writes,
and reviewing approve/writes policies in third-party .mcp.json files.
- An unbounded Reasonix wait now omits TaskOutput's optional timeout
instead of claiming 0 — in Claude's schema timeout is the maximum wait
in ms, so 0 would read as "don't wait", the opposite of the call.
- Structural input gaps (WebFetch prompt, NotebookEdit cell_number,
TaskOutput multi-job) are reported once per hooks file instead of once
per hook item, so a wildcard-matcher plugin gets one warning per gap
rather than one per hook; file-level wording also keeps the emitted
set deterministic across the random event-map iteration order.
- Real-spawn hook tests share one generous realSpawnTimeout: node cold
starts and first-run scans of a freshly built test binary can stall
for seconds on a loaded machine, and these tests assert behavior, not
latency. The timeout-path case keeps its tight budget on purpose.
Serialize delta resolution with rebuild publication, drain queued axes iteratively after failures, and restore the client's live config when a deferred apply fails.
Propagate switch-time transcript persistence failures across ACP, CLI, and serve so the outgoing controller remains active.
Propagate created-at and auto-title metadata cleanup failures so the title locator survives and a retry can converge. Add fault-injection coverage for both metadata files and assert auto-title state is removed.
Per-root cleanup removed the title entry before the fallible
title-sources cleanup. For a title-map-only topic (no sidebar index
entry) the title is the only thing that marks its root as a deletion
candidate, so a failure in the sources step stranded the sources and
created-at entries: the retry found neither an index entry nor a title,
skipped the root, and reported success with metadata still on disk.
Run the fallible sources cleanup (and the best-effort created-at and
auto-title-meta cleanup) before removing the title, and delete the
title entry last, so any failed attempt leaves the locator in place and
a retry converges on the fully-deleted state.
Cover title-only topics with a failure-then-retry regression test, and
tighten the indexed-topic retry test to assert the title survives a
failed attempt.
Verified against the Claude Code 2.1.209 tool schemas and the official
security-guidance plugin source:
- Extend tool_input key renames beyond the four file tools: notebook_edit
path->notebook_path, run_skill/read_only_skill name/arguments->skill/args,
bash_output/kill_shell job_id->bash_id/shell_id, and the dedicated
subagent wrappers' task->prompt, so Claude-authored guards reading those
fields no longer fail open on empty values.
- Synthesize Agent's prompt for parallel_tasks from its sub-task prompts
(keeping tasks alongside), bridging the structural mismatch so an
Agent-scoped guard inspects all dispatched work.
- Resolve relative file_path/notebook_path absolute against the payload cwd
(the same root Reasonix file tools resolve against), matching Claude's
"must be absolute" file-tool contract for prefix-matching guards.
- Deliver Bash tool_response in Claude's {stdout, stderr, interrupted}
shape instead of a bare string; the security-guidance plugin's
commit/push checks read exactly these fields and treat a non-object
response as empty, silently passing.
web_fetch needs no translation: its url field already matches Claude's.
A work-mode/model/effort switch splices the target profile's system prompt
into the carried history in memory only: the pre-switch Snapshot persists the
outgoing history, Resume/AdoptHistory never save, and session/close does not
snapshot again, so ACP switch -> close -> load (and serve/CLI restart+resume)
revived the outgoing profile's contract from disk while the session metadata
already claimed the new profile. Snapshot the adopted history right after the
controller swap lands on all three surfaces, after the swap so a failed swap
never leaves the old controller conflicting with a rewritten transcript.
pendingConfig was a single slot, so two axes changed during one active turn
both returned success and announced config_option_update, but only the last
delta survived to the post-turn drain. Queue one delta per axis
(last-write-wins within an axis only), resolve all queued axes into one state,
and apply them in a single rebuild; a rebuild claims only its own axes from
the queue so entries queued by other requests still drain afterwards.
emitModeDrift/emitToolApprovalDrift read controller state and swapped it into
the session without stateChangeMu, so a session/set_mode or tool-approval
selection completing between the read and the swap was read back as drift,
rolled the metadata back to the pre-selection value, and the next rebuild
re-applied the stale value to the replacement controller. Both emitters now
hold stateChangeMu across the read-and-swap.
Regression coverage: ACP switch -> close -> load system-prompt round-trip,
serve/CLI on-disk prompt assertions after a switch (verified failing before
the fix), per-axis pending merge during an active turn, a stale-drift-read vs
set_mode race with a pending rebuild, and a lock-contract test for both
emitters.
Round 4 of #6432:
- P1: switching tool approval to auto/yolo unconditionally cleared and
tombstoned the visible approval, but the backend's drainLocked keeps
fresh user decisions (plan/memory/sandbox escape) pending, and auto
keeps approvals an allow policy would not cover. The tombstone then
filtered every replay of the still-pending prompt, stranding the turn.
SetModeForTab/SetToolApprovalModeForTab now return the prompt ids the
switch actually auto-allowed (Controller.ApplyMode/ApplyToolApprovalMode)
and the frontend dismisses + tombstones exactly those.
- P2: submit_prompt_failed only compared prompt ids, so a late failure
from a pre-rebuild controller RPC could erase the rebuilt controller's
tombstone for the same numeric id and let a delayed replay resurrect an
already-answered prompt. approve/answerQuestion now capture the tab's
prompt epoch at RPC start and the reducer requires {id, epoch} to match;
controller_rebuilt and reset advance the epoch.
The outbound sanitizer replaced four known path prefixes inside Message
and Remediation and left every other field alone. Diagnostic prose embeds
user-controlled content — TOML parse errors quote config lines, URL
errors echo the full URL including credentials, MCP findings quote the
command line, permission findings quote rules — and Scope carries
provider/plugin names, so emails, tokens, and arbitrary paths reached the
selected provider unchanged.
Replace the blocklist rewrite with a strict allowlist DTO: findings ship
only severity, machine code, and a closed-vocabulary scope (derived:*
targets survive for rebuild plans; provider:*/plugin:* collapse to their
category; anything unrecognized becomes "other"), plus snapshot IDs,
update versions, and a fixed "<project>" root. No free-form diagnostic
string leaves the machine.
saveRepairTransaction returned the audit-log append error after
last-repair.json was already persisted. Snapshot restore treated that as
an uncommitted operation: it restored the original config and consumed
the new backup, leaving the persisted transaction pointing at a missing
file — every later UndoLastRepair failed — while the previous valid undo
had already been overwritten. UndoLastRepair itself likewise reported
failure after completing.
last-repair.json is the durable undo state and commits on persist; the
append-only audit log is best-effort and no longer fails the save.
The row title joins the meta line with the exact date the same way the
hover card does, so week-old sessions repeated the calendar date in the
classic aria-label and the creation tooltip. Route both through the
shared projectTreeDedupedExactTime helper.
The convergence sweep read every project's title metadata with hard
errors, so an unreadable file in an unrelated project aborted the
deletion after the target root's title was already removed — recreating
the partial-delete state the sweep was meant to eliminate, with every
retry failing on the same unrelated root.
Limit detailed cleanup to candidate roots: those whose sidebar index
lists the topic, plus any root whose title map contains it. The title
probe tolerates read errors on unindexed roots (they are skipped),
while candidate roots keep failing hard so damage on a root that
actually holds the topic is surfaced instead of silently half-cleaned.
The index/tombstone update still always runs, preserving retry
convergence.
Add a multi-project regression test with broken unrelated metadata.
- Guard trashTopic with an in-flight ref so double-clicking the quick
archive action cannot fire concurrent TrashTopic calls (same shape as
the frontend guard in #6404; backend idempotency stays with #6404).
- Keep the pin entry out of the creation-mode context menu; pinning
reorders the shared classic/workbench trees, and creation mode is
meant to keep its original behavior.
- Stop the classic hover card from repeating the calendar date for
sessions older than a week, and route the week-old activity label
through the same locale-aware date formatter as the exact date.
The failed-installer marker means the installer may have left the release
unit partially replaced, so the live binaries cannot be presumed coherent.
The launch gate keyed on MixedInstall, which only reports whether this
rollback failed to compensate its own swaps — a rollback that failed while
staging returns MixedInstall=false while leaving the installer's
half-written unit in place, and Guard went on to start it.
Block startup on every RecoverFailedInstall error unless the restore
actually completed (RolledBack with only the marker cleanup failing).
The failure cleanup renamed the backup over dest — but the fallback that
creates a byte-copy or recreated-link backup is entered precisely when
that rename cannot work (the state dir sits on another filesystem), so the
cleanup's retry of the same rename failed too, its error was discarded,
and the transaction-save path had already removed dest: a failed restore
could silently delete config.toml.
Restore the backup with a node-aware operation instead: rename first, then
per-type recreation (a symlink rebuilt beside dest and renamed into place,
a regular file rewritten atomically), never removing dest before the
replacement can land, consuming the backup only on success, and surfacing
a cleanup failure joined to the original error with the backup path.
deleteTopic keyed the whole cleanup on the title entry: once a failed
attempt had removed the title, a retry hit the idempotent-success branch
and returned nil without cleaning the title sources, created-at entry,
sidebar index, or delete tombstone, leaving a ghost topic that kept
rendering in the tree and could never be deleted.
Sweep every step independently instead: each root's title, source, and
created-at entries are removed when present, and the index/tombstone
update always runs, so a retry (or concurrent duplicate) finishes
whatever a previous attempt left behind. deleteTopicCreatedAt now skips
its save when the entry is absent so the all-roots sweep does not
rewrite untouched files, and the sweep also clears the per-topic
auto-title meta entry that deletion previously leaked.
Add failure-then-retry and leftover-state regression tests.
TestMain pins REASONIX_STATE_HOME for the whole test binary, so the test's
REASONIX_HOME isolation was inert and the retained crash file leaked into
the shared state dir, failing the hang-watchdog test that runs later
(hang reports do not overwrite an existing crash). Follow the sibling
tests' cleanup convention instead.
A pending mutation restored from a persisted delivery checkpoint uses a -1
review baseline, but HasSuccessfulReviewAfter rejected every negative index,
so fresh read/diff receipts could never satisfy final readiness and the
checkpoint never cleared. Treat a negative index as the restored-checkpoint
baseline: the mutation's touched paths are unknowable, so any successful
review-shaped receipt (read, or a git diff/status inspection) counts, matching
the -1 semantics of the complete-step and sign-off matchers.
The delivery recovery notice appears for every final_readiness outcome, but
the continue handler required ResumeGoalForTab to succeed before submitting,
turning the button into a no-op for ordinary Delivery sessions with no Goal.
Route the flow through continueDelivery: tabs without a Goal submit the
continuation directly again, Goal tabs still resume first, and a refused
resume (completed Goal) or a mid-flight tab switch drops the send.
Regression coverage: checkpoint-baseline review semantics in the evidence
ledger, an end-to-end restored-pending-mutation-to-completion agent test, and
behavior tests for every continueDelivery path.
RestoreConfigSnapshot read the live config through the symlink and backed
up a plain-file copy, then atomically replaced the link itself — so after
restore + undo a dotfiles-managed config.toml was permanently severed into
a regular file.
Back up by renaming the original node aside instead (with a node-preserving
cross-device fallback), mirroring the quarantine path: the restore still
materializes the snapshot as a plain file without writing through the link,
and undo recreates the original symlink exactly.
The rollback restored release-unit binaries one by one after hash
verification, so a failure on a later file left earlier files on the old
build and the rest on the new one — and Guard then switched to Safe Mode
and launched that mixed old-desktop/new-Guard install anyway.
Restore now stages every backup next to its target first (a copy can fail
halfway; staging leaves the live binaries untouched), then swaps via
renames only, unwinding already-swapped binaries when a later step fails
so the unit stays coherent on the new version and the retained pending
transaction can retry. Only when that unwinding itself fails does the
result report MixedInstall — and Guard now refuses to start the desktop
in that state instead of continuing into Safe Mode.
A Safe Mode boot skips loading desktop-tabs.json and builds one temporary
Global tab, but the controller-build path still wrote that tab back to
desktop-tabs.json, replacing the user's saved layout with a single random
tab. Gate the tab-snapshot write — the single choke point every save
funnels through — so Safe Mode tabs stay ephemeral.
The recovery-branch GC has the same blind spot: with no tabs loaded, every
normally-open session looks closed, so idle branches the real layout still
owns would be swept to the trash. Recovery boots no longer GC.
Safe Mode boots from built-in defaults and never reads the user's config,
so it cannot see a telemetry/metrics opt-out recorded there — it started
the metrics aggregator and flushed pending metrics/crash payloads with the
defaults' enabled values, silently bypassing the user's privacy choice.
Force telemetry and metrics off in the Safe Mode config, and keep the
pending metrics/crash flushers from running at all in Safe Mode: whether
to send or drop those payloads depends on the user's real preference,
which only the next normal boot can read.
Resolves internal/boot/boot.go against #6461's Economy tool-source
connector: Safe Mode registers neither install_source/skill tools nor
connect_tool_source, so a recovery boot cannot re-expose skills,
commands, memory, or MCP through the connector.
A leading double backslash (\\server\share, \\?\C:\..., \\.\...) is a
path prefix, not a shell escape pair; unescapeWindowsShellPath collapsed
it to a single backslash, so escaped backslash-UNC and extended-length
spellings never produced an existing-file candidate.
websocket.DialConfig dials with context.Background(), so cancelling the
adapter context could not interrupt a TCP dial or a stalled WebSocket/TLS
handshake. Stop waits on loopWG, and the conn only becomes trackable after
the dial returns, so closeConn had nothing to close and shutdown or a
runtime rebuild could block indefinitely.
Dial with cfg.DialContext(ctx) so Stop's cancellation aborts the dial
phase, and add a regression test that stops the adapter while a local
server holds the handshake stalled.
Bring the workbench pin, archive, and unread-attention behavior to classic topic rows. Read topics expose the existing quick actions on hover or keyboard focus, while unread topics reserve the right edge for the blue activity dot and runtime session rows remain action-free.
Classic action focus also dismisses the delayed hover preview, and the side column reserves enough width to keep long labels clear of the two controls. Creation mode retains its existing behavior.
Classic topic rows drop the second meta line and adopt the side-time
recipe the other layouts already use: title on the left, compact
activity time right-aligned on the same 34px line, with the workbench
swap rules for the shortcut badge and running-state dot mirrored under
a new app--classic scope. Paused/error keep their inline status pill;
waiting shows the shared amber side pill. Turns and the exact date stay
in the hover preview card and the row aria-label.
Also expose pin/unpin in the classic topic context menu: classic
sorting already ranks pinned topics first, but the only pin entry was
gated to workbench, leaving classic users no way to set it. The
now-unused time-only meta variant is removed.
Bring three sidebar patterns from the Codex desktop app to the classic
layout so busy projects stay scannable:
- Topic rows keep only the activity time inline; turns and the exact
date move to a hover preview card (full title, status, turns, date,
project) rendered through a body portal so the sidebar overflow
cannot clip it. The full string stays on the row as its aria-label.
- Each folder previews its first five topics behind a show-more /
show-fewer toggle. Truncation pauses during search and time filters,
and a folder auto-reveals when the active session sits in its tail.
- Empty project folders become expandable and show a faint
"no sessions" placeholder row instead of being inert.
Workbench and creation layouts keep their existing row recipes.
Classic layout previously rendered topics in persisted insertion order
while each row displayed its last-activity time, so the list looked
unsorted whenever an older topic was updated recently. Arrange classic
folders through the shared workbench sorter (project order preserved,
pinned topics first) so row order matches the time shown in the meta
line, and surface the existing created/updated sort criteria in the
classic filter menu. Creation mode keeps its fixed updated ordering.
The sort choice shares the projectTree:workbenchSort storage key, so
existing saved preferences keep working across layout styles.
formatSelectionReference fenced the selection body adaptively but put
the file path in a single-backtick code span. Backticks and newlines are
legal in file names and would terminate the span early, spilling the
path into plain instruction-like text. The path is now a JSON string,
which escapes quotes, backslashes, and control characters; tests cover
backtick, CR/LF, quote, and backslash names.
Stop only cancelled the context, but the gateway loop blocks in
websocket reads that do not honor ctx, and nothing closed the saved
connection or waited for the loop to exit. Every bot-runtime rebuild
could therefore leave a zombie goroutine holding a live QQ gateway
session that keeps receiving dispatches.
Stop now cancels, closes the tracked connection to unblock the read,
and waits for the loop. Publication of the tracked conn is refused once
ctx is cancelled so a dial racing Stop cannot leave an unreachable
connection, and RunWithRetry's documented "attempt MUST honor ctx"
contract is upheld.
closeBotSessionState read state.cancel outside gw.mu while runTurn
rewrites the field under the lock on every turn, so a Stop (or session
rebuild) racing a turn between session pickup and cancel publication was
a data race. Close now snapshots and clears the cancel inside gw.mu and
invokes it outside, and runTurn refuses to publish onto a state that was
already unlinked, cancelling that turn immediately instead of leaving it
uncancellable.
Also documents the callback reentrancy contract: GatewayConfig callbacks
run on gateway-owned goroutines that Stop drains, so calling Stop from a
callback deadlocks; hosts must trigger shutdown asynchronously.
The new regression test pins the pre-publication window with -race; the
existing lifecycle tests only stopped after RunTurn had begun.
The repair quarantine moves the link itself aside, but the copy-based undo
followed it with ReadFile and materialized a regular file at the target,
permanently severing dotfiles-managed config links. Recreate the link via
Lstat/Readlink/Symlink (a single atomic syscall, so the crash-safe resume
contract is unchanged), and use Lstat for the preflight and redo checks so
dangling links are still treated as present.
planmode.Marker promises todo_write for maintaining planning state, but
Economy kept the whole workflow source fail-closed in plan mode because
it bundles complete_step. Admit the workflow source in plan mode and
narrow its installer to todo_write while planning; complete_step still
requires a fresh workflow connect outside plan mode. Document the subset
in the tool contract and cover the full plan/execute loop.
- Keep every undo backup on disk until the per-change progress record is
persisted (restore by copy, then mark, then remove), so a crash between
the restore and the progress write can no longer strand an unresumable
transaction; completed-but-uncleaned steps are swept on retry
- Record a Windows installer failure from the update helper (which runs
outside the validated install directory and must not restore binaries
itself) and let Guard roll back the release unit on its next launch
instead of leaving a mixed install behind a clean-exit state
- Make redo names unique per change (index + nanosecond stamp) so one undo
touching the same target twice keeps both redo copies
- Serialize startup-state read-modify-write cycles with a cross-process
file lock and refuse transitions against a live foreign owner, closing
the concurrent cold-start race
- Reject file update transactions with missing backup hashes and require
the release unit to list the primary executable; rollback errors on any
unhashed entry instead of skipping verification
Mark the remaining backslash-space source cases as POSIX-only so Windows continues to validate its native path rules without expecting shell unescaping.
Co-authored-by: SivanCola <[email protected]>
- Back up and restore the complete Windows/Linux release unit (desktop
executable plus Guard/launcher siblings), verify every backup hash before
restoring, and roll back instead of deleting the rollback metadata when the
Linux apply fails after the Guard binary was already replaced
- Stop counting duplicate launches as crashes: StartupTracker.Begin no longer
overwrites state while the recorded owner PID is alive, since Wails second
instances exit through os.Exit without OnShutdown
- Record multi-action AI repair plans as one plan-level transaction so undo
reverts the whole plan (or the applied prefix on mid-plan failure), and
persist per-change undo progress so an interrupted undo can resume
- Skip legacy memory/session migration and cleanup-pending deletion in Safe
Mode to preserve the recovery isolation boundary
- Load config read-only in guard diagnose/assist so documented read-only paths
never rewrite legacy MCP tier lines on disk
Use target-OS path cleaning for the injected parser helper and keep POSIX shell-escape integration tests off Windows, where backslashes remain native path separators.
Co-authored-by: SivanCola <[email protected]>
The example config pinned the old default system prompt, so copying it
would override the new lean built-in default; replace it with a commented
placeholder. Docs claimed the tool schema stays stable within a profile,
but in Economy each successful connect_tool_source adds schemas on the
next request; state the accurate per-connection prefix semantics in the
English and Chinese guides and the collaboration-modes doc.
Keep interactive tab strip descendants no-drag while allowing only unused classic chrome space to drag the window on macOS and frameless Windows. Add source-level coverage for the drag-region boundary.
Refs #6251
Co-authored-by: SauronSkywalker <[email protected]>
A character whitelist can never keep up with what zsh/bash escape
(\^ and \, were already missing): in POSIX shells a backslash simply
makes the next byte literal. Drop shellEscapedPathByte and unescape any
backslash pair; a trailing backslash stays literal. The Windows branch
still skips unescaping entirely, and the whitespace detector and token
splitter already used full semantics, so this also removes an internal
inconsistency.
Resolves conflicts with the rich-input composer refactor: Composer now
derives at-token state from activeFileReferenceToken (already
escape-aware), ListDirForTab gets the unescaped directory, and the
ref-token test joins the reorganized frontend test chain.
The desktop UI kept its own whitespace-delimited @token regexes, so a
CLI-produced ref like @docs/my\ file.md truncated in menu detection and
display badges, and the composer's inline fallback inserted refs that
could not resolve on submit. Mirror internal/control/refs.go in a shared
lib/refToken helper:
- active-token detection (FileReferenceMenu, Composer) keeps escaped
space/tab inside the token; frag is unescaped for entry matching and
ListDir gets the real directory path
- inline insertion (pickInlineFileReference, composerPickFileEntry
fallback) escapes whitespace; structured workspaceRef keeps the real
unescaped path
- display badges (attachmentDisplay) capture the full escaped token and
show the unescaped path; workspace edit-replay serialization escapes
whitespace so the round-trip is lossless
- Windows backslash separators stay literal throughout
Full-mount regression for the cross-scope routing fix: open the preview
via revealPathRequest, pop the selection toolbar with a real DOM
selection + mouseup, and right-click blank tree space, then assert a
tabId/workspaceScopeKey rerender discards both menus. Before the fix
only an incidental dir-reload identity change cleared the first two;
the blank-tree menu survived scope switches (this suite failed).
Same defect shape as the transcript selection overlay: the selection
toolbar and tree menus capture text/paths from the previous scope while
add-to-chat routes to the tab active at click time. A keyboard tab
switch fires no mousedown/scroll/Escape, so a surviving menu would add
the old scope's selection to the new session.
The selection overlay captures only text, while onAddToChat resolves the
active tab at click time — so a selection made in session A could land
in session B after a keyboard tab switch, and the overlay stayed armed
while hydratePlaceholderActive rendered the previous transcript as a
loading placeholder.
Gate the overlay off during placeholder hydration and reset the floating
action, copy menu, and Escape-dismissal memory whenever the source tab
(resetKey) changes.
Closes the gap where a pasted or completed @reference to a path with
spaces inserted fine but silently failed to resolve on submit, because
the @token grammar stopped at the first space:
- parseRefTokens now treats backslash-escaped space/tab as part of the
token (dropping the backslash); any other backslash stays literal so
Windows separators keep their meaning
- EscapeRefPath/UnescapeRefPath are the exported inverse pair
- pastedFileRef, FileRefLine, and @-completion inserts escape whitespace
in the refs they produce; completion's token scanner and directory
reads understand the escaped form so descending through a spaced
directory keeps working
- unescaped tokens parse byte-for-byte as before, so existing
transcripts and typed refs are unaffected
Integrate the workspace selection flow from PR #6098 on top of the
selected-text context mechanism:
- Selecting code in the workspace preview pops an inline Add-to-Chat
toolbar on mouseup (the right-click menu stays as a fallback), and
FloatingMenu portals to <body> so a transformed ancestor cannot
displace its fixed positioning.
- Workspace selections ride the quoted-context serialization as
{path, text} entries instead of raw composer text, so file paths are
preserved and instruction-like comments in repository code stay
quoted context rather than user text.
- In plan-revision mode the snippet arrives as fenced Markdown via
request.text — the revision input only consumes text, so a structured
block would be dropped silently; the fence outgrows embedded backtick
runs so content cannot forge the closing marker.
- Selection badges localize at render time and show the file basename
for code selections.
Co-authored-by: sudoviber <[email protected]>
Review follow-up for #6060:
- unescape shell escapes even when the pasted path has no whitespace
(e.g. /tmp/capture\(1\).png), so the image saves instead of failing
with a consumed paste
- split pasted text into path tokens with shell rules (escapes and
token-leading quotes) instead of strings.Fields, so multiple escaped
or quoted paths on one line each become an image token
- skip unescaping on Windows to preserve native backslash path
semantics; inject GOOS so both branches are tested everywhere
The prior regression test armed the loop-guard pass directly. Drive the
whole failure loop from the review finding through Run() instead: three
consecutive plan-mode-blocked attempts at a required write-capable MCP
capability arm the guard via the storm breaker, and the following
blocker report must be accepted rather than ending in FinalReadinessError.
Address review feedback on the external opener menu:
- Reap launched opener processes in a background Wait so exited
children no longer accumulate as zombies (matches the existing
notify sender pattern).
- Launch the application before persisting the preference so a
config-write failure cannot block the launch and a failed launch
never becomes the saved default; the two failure paths now show
distinct toasts.
- Cover the Linux core paths: .desktop parsing (Hidden/Type/section
scoping), alias matching, gio launch fallback, per-terminal
working-directory arguments, and absolute icon resolution.
Cmd/Ctrl+L was hardcoded in TranscriptSelectionMenu, invisible to the
settings conflict check: rebinding another action onto the same combo
would fire both handlers on one keypress. Register selection.addToChat
in SHORTCUT_DEFINITIONS, drive the handler through useGlobalShortcut
(gated on overlay visibility), and derive the visible hint from the
resolved combo so it tracks rebinds.
Escape now dismisses the floating action while keeping the browser
selection; the dismissed selection stays hidden through the trailing
keyup until it changes or a new pointer gesture lands.
insertImageRef also replaces an active composer selection, so the
direct image paste, clipboard image, and markdown-image attach paths
need the same shouldClearWideInputChange guard as text pastes to avoid
ghost cells on Windows after replacing a wide-rune selection.
- Keep the composer selection active across Ctrl/Cmd+V so the async
clipboard result replaces it instead of inserting at the cursor.
- Collapse the selection to its ordered start/end on Left/Right instead
of moving one extra character from the drag head.
- Highlight a selected newline on the trailing caret space rather than
the previous character.
- Run the wide-input repaint check on the paste early-return paths and
drop the ineffectual inputBeforeSelection assignments (ineffassign).
The plan-mode early return in finalReadinessCheck assigned the capability
gate failure directly, skipping the loopGuardAllowsFinal() pass that lets
the model report a host-blocked required capability. A required writer
Skill/MCP blocked by plan mode itself could therefore never satisfy the
gate and the turn ended in FinalReadinessError instead of a blocker
report. Honor the loop-guard pass in the plan branch, matching the two
execution-path call sites, and add a plan+delivery+required-capability
regression test.
Add grapheme-aware composer hit testing, editable selection behavior, and regression coverage for wrapped, wide, multiline, clipboard, and failed-paste paths.
jsonschema/v6's default compiler loader is FileLoader, so a tool schema
with a file:// $ref was resolved from local disk during validation. If
the referenced file held a valid schema, the tool passed validation and
its unresolved external ref could still 400 the whole provider request,
breaching the no-external-resources boundary this validator promises.
UseLoader(nil) keeps registered resources and the embedded metaschemas
resolvable while every other URL now fails compilation, quarantining the
tool instead.
MCP 2024-11-05 and the Anthropic/OpenAI tool contracts require a tool's
input schema root to declare type "object", but ValidateToolSchema only
checked that the root parses as a JSON object, so schemas like
{"type":"string"} or {"type":["object","null"]} still reached the
provider tool list and could 400 the whole session.
Enforce root type == "object" in ValidateToolSchema. To avoid
quarantining the many real-world tools that omit the root type entirely
(bare {} or properties-only schemas), CanonicalizeSchema now makes the
omitted root type explicit before validation, matching the existing
empty-schema normalization; explicitly declared non-object roots are
quarantined instead of silently rewritten.
Control #1 (#6442) accepts any package-name-shaped source, which is right
for kind=mcp (npx -y <pkg>) but wrong for kind=skill: install_source only
reaches the npx-package branch for kind auto/mcp, so a bare name like "123"
published as a skill resolves to an MCP server, not a skill — it slipped
through the syntactic gate. Guard it in PublishSchema so a skill source must
be a SKILL.md URL or a GitHub repo path.
The publish form stored any string as `source`, so submissions like a
free-text "这个来源是自己制作的" reached the review queue only to be
rejected for being uninstallable — install_source can't classify them, so
they were dead on arrival. Mirror install_source's own acceptance
(internal/installsource/names.go: http(s) URL, git:github.com/… shorthand,
or package name) in PublishSchema and refuse anything else — free text or a
publisher-local path — at publish time, with a matching client-side pattern
hint on the form.
Bring the crash worker under test: add vitest + a validation suite and wire
typecheck + test into the deploy step so a bad schema can't ship to
crash.reasonix.io. This is the worker's first test infra.
Repo-wide sweep for PR #6438's two confirmed defect shapes found the same
bugs at every other controller-rebuild site for a model/effort/work-mode/
settings switch:
- AdoptHistory (CLI /model, /effort, /work-mode, skill-refresh; serve's
switchModel/switchEffort) silently overwrote the freshly built
controller's own profile-specific system prompt with the carried
(stale) one, so the model kept following the outgoing profile's
contract after every switch. Desktop already had its own equivalent
fix (resumeWithFreshSystemPrompt) and needed no change here.
- Every rebuild site across CLI, serve, and desktop (SetModelForTab,
SetEffortForTab, SetTokenModeForTab, rebuildSettingLocked) dropped
same-session "Allow for this session" tool grants and Plan-mode
read-only command trust, forcing the user to re-approve things
already granted this session after any switch.
Reuses Controller.SessionAuthorizations/RestoreSessionAuthorizations
(added by the prior commit) and wires them, plus the leading-system-
message splice, into every affected rebuild site.
Cherry-picked from fix/controller-rebuild-siblings (origin/main-v2 base);
resolved one conflict in internal/cli/cli.go where this branch predates
the unrelated --dir workspace-root threading from PR #6283.
Review of PR #6438 found four bugs in the runtime-profile/model/effort
rebuild path: AdoptHistory silently overwrote the freshly built controller's
own profile-specific system prompt with the carried (stale) one; a config
switch queued while another axis was mid-rebuild resolved its full snapshot
from the not-yet-updated baseline and could roll that axis back once
applied; finish() exposed the session as idle before controller-side mode
drift (e.g. Plan auto-exit) was reconciled, letting a concurrent switch
rebuild from the stale modeID; and a rebuild dropped session-scoped
"Allow for this session" tool grants and Plan-mode read-only command trust,
contradicting the PR's own "preserving permissions" claim.
Fixes: splice the new controller's leading system message into the carried
history before AdoptHistory; replace the pendingConfig full-snapshot queue
with a per-axis delta that re-resolves against live state at apply time;
reconcile mode/tool-approval drift before finish() marks the session idle;
and add Controller.SessionAuthorizations/RestoreSessionAuthorizations to
carry approval-manager grants across the swap.
Round-3 review of #6432 found the resolvedPromptId guard (9ed15099b) had
two regressions of its own:
- A controller rebuild (model/effort/token-mode switch) reissues
approval/ask ids from "1" (per-controller counters, per sound.ts), but
nothing reset resolvedPromptId/promptArrivedId/promptArrivedAt across
the rebuild. A genuinely new prompt from the rebuilt controller reusing
an old id was silently swallowed as a stale replay.
- clearApproval/clearAsk write the tombstone optimistically before the
ApproveTab/AnswerQuestionForTab RPC resolves, and its rejection was
swallowed. If the RPC actually failed, the prompt was still genuinely
pending server-side, but any later replay of that id was now permanently
ignored — losing the approve-failure recovery the replay design exists
for (#3844).
Add a controller_rebuilt action (wired to onRuntimeRebuilt) that drops the
id-anchored bookkeeping per rebuilt tab, and a submit_prompt_failed action
that undoes the tombstone (and proactively requests a replay) when the
backend call actually fails.
Fifth review round on #6434 found two more P1 gaps in the Claude hook
compat layer:
- marshalPayload forwarded Reasonix's tool-call arguments verbatim as
tool_input. Reasonix's Read/Write/Edit/MultiEdit use a "path" key; Claude's
own tool_input schema (confirmed against the current hooks reference docs)
uses "file_path" for those same tools. A guard hook reading
".tool_input.file_path" therefore saw an empty value and could fail open
even though its matcher correctly fired. Bash's "command" and Glob/Grep's
"pattern"/"path" already match Claude's field names by convention, so only
the file-path-taking tools needed a rename; NotebookEdit's cell_number
(0-based index) vs Claude's cell_id (opaque identifier) is a deeper
structural difference left untranslated rather than guessed at.
- Only "task" mapped to Claude's "Agent" tool. Reasonix has six more
subagent-spawning entry points that reach PreToolUse under their own
names: read_only_task, parallel_tasks, and the dedicated explore/research/
review/security_review wrapper tools (BuiltinSubagentTools in
internal/skill/tools.go — confirmed these are independently-registered
tools, not calls routed through run_skill). A Claude plugin's "Agent"
safety matcher silently missed all of them. All seven now map to "Agent"
(with the legacy "Task" matcher alias preserved). Also mapped
read_only_skill -> Skill, alongside run_skill, per the same audit.
Co-authored-by: SivanCola <[email protected]>
The previous fix only threaded the CLI's --permission-mode into
boot.Build for the one-shot `reasonix run` path. Interactive sessions
(chat REPL, desktop, ACP) boot with no HeadlessApprovalMode and switch
modes later at runtime via SetToolApprovalMode (Shift+Tab). That call
only rebuilt the parent executor's gate (refreshInteractiveGate); the
task/read_only_task, writer-capable skill sub-agents, and planner
runner kept the gate captured once at boot construction, with no
rebuild hook. Under interactive auto, the parent correctly failed
closed on an explicit ask rule, but a task sub-agent hitting the same
rule stayed on the boot-time default (ask resolves to allow) and ran
it silently.
Replace the frozen *freshHumanHeadlessGate boot handed to every
sub-agent surface with control.SharedHeadlessGate, a mutable,
concurrency-safe holder every consumer reads through by the same
pointer. SetToolApprovalMode and ApplyHeadlessApprovalMode now call
Update on it (via the new Options.SubagentGate wiring) so a runtime
mode switch reaches sub-agents the same way it reaches the parent.
Fourth review round on #6434 found two P2 accuracy gaps once the two P1
security bypasses were confirmed fixed:
- claude_compat.go accepted a hook's matcher without checking whether it
names a real Claude tool Reasonix has no equivalent for. "WebSearch"
(no Reasonix web-search tool) and "ExitPlanMode"/"EnterPlanMode" (plan
approval is a controller-level decision, never dispatched through the
tool-call hook path; confirmed by tracing permissionRequestHookPayload's
explicit exclusion of planApprovalTool) are two concrete, independently
verified examples: a hook scoped to only these matchers would never fire
while the package still reported "full" compatibility. A plain
tool-name matcher, or a "|"-alternation of them, naming only tools from a
conservative, individually-verified unsupported set now downgrades the
package to "partial" with a structured warning; matchers using other
regex syntax, or naming even one tool Reasonix does support, are left
unevaluated to avoid false positives. Also fixed a real gap found while
auditing this: "run_skill" (Reasonix's skill-invocation tool) was never
mapped to Claude's "Skill" tool name, so a "Skill"-scoped hook silently
never fired either — same bug shape as the task/ask fixes from the
previous round.
- Added a regression test exercising the opts.fresh-only branch of the
fresh-human approval fix (PlanModeReadOnlyCommandApprovalTool, which is
fresh via requestFreshApprovalDecision without being one of
RequiresFreshHumanApprovalTool's fixed cases) — the existing tests only
covered tools protected through requiresFreshApprovalTool(tool).
Co-authored-by: SivanCola <[email protected]>
Round-2 review of #6432 found the idle-rejection backstop was necessary
but not sufficient: it only reconciles when a fetched snapshot is
rejected as stale, but a delayed replay of an already-answered prompt
can also arrive with no snapshot rejection ever happening (idle applied
before the replay lands, or a fresh running=true/pendingPrompt=false
snapshot that never touches approval/ask). Either way turn_done's
keepPlanApproval then resurrects the zombie approval permanently.
Track the id of the most recently user-resolved approval/ask
(resolvedPromptId, set by clearApproval/clearAsk) and ignore any
approval_request/ask_request replay carrying that same id outright,
instead of re-arming it and hoping a later snapshot disproves it.
ApplyHeadlessApprovalMode only rebuilt the parent executor's gate.
boot.Build separately baked a mode-unaware default gate (nil approver,
ask resolves to allow) into every headless-only surface it
constructs — task/read_only_task, writer-capable skill sub-agents,
and the planner runner — with no way to rebuild it later. Under
--permission-mode auto, an explicit ask rule correctly denied a
parent write_file call but a task sub-agent hitting the same rule was
silently allowed, bypassing the boundary auto is supposed to enforce.
Extract the mode-to-gate mapping into control.BuildHeadlessApprovalGate,
the single construction point ApplyHeadlessApprovalMode and boot.Build
both use, and thread the CLI's --permission-mode through
boot.Options.HeadlessApprovalMode so every sub-agent gate matches the
parent's contract from construction instead of only the top-level
executor getting a post-hoc rebuild.
Third review round on #6434 found a real regression from the prior fix, plus
two remaining accuracy gaps:
- [P1, security] A Claude PermissionRequest hook's "allow" decision (added in
the previous fix) was honored unconditionally, including for tools that
RequiresFreshHumanApprovalTool deliberately excludes from YOLO/auto-approval
and Guardian: sandbox escapes, Reasonix config writes, and memory
remember/forget. A broadly-matched plugin hook returning allow could
silently rubber-stamp any of those. Auto-allow is now ignored for
fresh-human-required tools (falls through to the normal interactive
prompt); a hook's deny still applies universally since refusing is always
safe to auto-honor.
- [P1, security] The Claude subagent tool was renamed "Task" -> "Agent"
(confirmed against the current https://code.claude.com/docs/en/tools-reference);
Reasonix's "ask" tool (AskUserQuestion's equivalent) wasn't mapped at all.
A matcher or tool_name check written against either current name silently
never fired. Now emits the current name (task -> Agent, ask ->
AskUserQuestion) while still matching hooks whose matcher uses the legacy
"Task" alias.
- [P2] Documented, at both the code (compatibilityFor) and doc level, exactly
what "full" compatibility does and doesn't guarantee: it reflects
successful static mapping of declared capabilities, not full runtime
parity — updatedInput and PreToolUse's ask/defer decisions are chosen by a
hook's stdout at call time and can't be flagged during install the way
if/asyncRewake/Stop/SubagentStop already are.
Co-authored-by: SivanCola <[email protected]>
Second review round on #6434 found two more hook security gaps and an
over-claimed compatibility status:
- imported Claude hook matchers (e.g. "Bash", "Write|Edit") and the
tool_name field a hook script inspects were left in Reasonix's own
vocabulary (bash, write_file, ...), so a matcher-scoped or tool_name-gated
Claude hook silently never fired. Added a name-translation layer
(claudeFacingToolName) applied only to PayloadFormat "claude" hooks, both
at match time and in the emitted payload.
- UserPromptSubmit's standard exit-0 JSON block (top-level
{"decision":"block","reason":...}, distinct from PreToolUse/
PermissionRequest's hookSpecificOutput shape) was not parsed, so a hook
denying a sensitive prompt via JSON instead of exit 2 was ignored.
- PermissionRequest's "allow" decision (auto-approve without prompting) was
never implemented — only the deny half of Claude's auto-answer contract
from the prior fix. Runner.PermissionRequest now returns a tri-state
decision (nil/deny/allow) and the approval flow honors all three.
- claude_compat.go now parses the "if" and "asyncRewake" hook fields and a
Stop/SubagentStop hook import, and downgrades compatibility to "partial"
with a structured warning instead of claiming "full" for semantics
Reasonix doesn't evaluate (conditional execution, wake-on-exit-2 async,
and block-the-turn respectively). The hook is still imported best-effort.
An existing test (TestPlanClaudeCompatibilityReportsAgentsHooksAndMCP,
modeled on the PR's own agent-critter example) had asserted "full"
compatibility for exactly this Stop-hook case; updated to the corrected
"partial" expectation.
Co-authored-by: SivanCola <[email protected]>
The rebase onto #6283 folds WorkspaceRoot into cliBuildOverrides, so --dir now
sets boot.Options.WorkspaceRoot and --add-dir relative paths resolve against it.
Add a regression test proving a relative --add-dir resolves under the explicit
--dir, not the enclosing git root.
Second review round on the headless approval mode: auto and yolo shared one
nil-approver gate, so a command the config explicitly marked "ask" ran silently
under `reasonix -p --permission-mode auto`, violating auto's "preserve ask rules"
contract (interactive auto prompts on those and never auto-approves them).
Split the gates: auto uses the deny approver so an unanswerable ask decision
fails closed while the ordinary writer fallback is still auto-approved; only
yolo/bypassPermissions keeps the nil-approver bypass; dontAsk still denies both.
Document the non-interactive resolution of every mode in CLI.md / CLI.zh-CN.md,
and update the tests to assert auto denies an explicit ask rule, auto allows the
writer fallback, and yolo bypasses the ask rule.
Address six review findings on the CLI interactions work:
- run: map --permission-mode onto a non-blocking headless gate instead of
installing the interactive approver/asker. `reasonix run` has no key loop and
the approval timeout defaults to infinite, so auto/dontAsk/yolo previously
wedged forever on an Ask rule, the `ask` tool, or a sandbox/config approval.
New Controller.ApplyHeadlessApprovalMode swaps the gate only (auto/yolo allow
the writer fallback, dontAsk denies what would ask); deny rules and fresh-human
tools stay enforced.
- permission: strip session-allow (--allowed-tools) entries that target tools
requiring fresh human approval, so `--allowed-tools remember` can no longer
bypass the always-prompt memory contract via the interactive gate.
- run: send the --copy notice to stderr under --print / structured output so
`--output-format json` stays a single machine-readable object.
- routing: honor -p/--print when it trails other top-level flags, so
`reasonix --model X -p "task"` dispatches to `run --print`.
- tui: restore the legacy numeric 4 as deny on fresh two/three-choice approvals.
- tui: handle Ctrl+P/Ctrl+N in the completion menu, matching the documented keys.
Adds focused regression tests for each fix.
Co-authored-by: 司空祎宸 <[email protected]>
Second-round hardening for #6432. The prompt-freshness guard plus id
anchor still had a hole across a navigation boundary: on a rapid
A→B→A switch (or a single-surface state wipe), backend_activation_start
drops the prompt anchor, so a delayed replay of an already-answered
prompt is treated as a brand-new prompt and re-anchors to a late time.
The authoritative idle ListTabs result fetched right after the answer
then looks stale and is discarded, and turn_done's keepPlanApproval
resurrects the resolved exit_plan_mode card — the input locks again.
A frontend anchor alone cannot distinguish genuine recovery of a
still-pending prompt from a delayed replay of a resolved one, because
navigation can erase the anchor the distinction relies on. Add the
authoritative backstop: whenever dispatchRuntimeStatusForTab rejects a
stale idle snapshot, schedule a single debounced reconcile that refetches
backend state. If the backend resolved the prompt, the fresh snapshot
(taken after any in-flight replay) is newer than the anchor and clears
the zombie; if the prompt is genuinely pending, the fresh snapshot keeps
it and re-replays. This converts a potentially stuck zombie into one that
self-heals within STALE_PROMPT_RECONCILE_MS (150ms).
The reducer intentionally still rejects the stale snapshot (it cannot
authoritatively resolve the ambiguity in-place); the hook-level reconcile
is the authority, matching the reviewer's "reject then reconcile" option.
Tests: add a hook-level convergence case driving the exact reverse race
(snapshot fetched before the prompt, dispatched after; backend reports
idle) and asserting the scheduled reconcile clears the zombie and unlocks
input. Mutation check: disabling the reschedule fails the two convergence
assertions (zombie survives, input stays locked).
t.Cleanup runs LIFO. The cwd-restore cleanup was registered before
t.TempDir(), so its own RemoveAll cleanup fired first while the process
cwd was still inside the temp dir being removed — Windows refuses to
delete a directory that is the process's current working directory.
Register the restore after t.TempDir() so it runs first and cwd is back
outside the temp tree before RemoveAll runs.
setupSubagentCommand hardcoded an empty workspaceRoot regardless of --dir,
so subagent run/try inside a nested directory of a git repo fell back to
git-root detection and got broader access than the explicit --dir target
(e.g. --dir /a/b under /a/.git resolved to /a). Thread
workspaceRootForDir(*dir) through the seam the same way runAgent and
chatREPL already do, and fail loudly on a Getwd error instead of
silently widening scope.
Add regression coverage for the real plumbing (not just the helpers):
subagent run/try --dir now asserts the explicit root reaches the setup
seam unchanged, and a new cli-level test drives chdirTo ->
workspaceRootForDir -> setupProfile -> boot.Build to prove the initial
controller pins an explicit --dir root and still falls back to the
git root when --dir is empty.
Review on #6434 found that an imported Claude PreToolUse/PermissionRequest
hook's deny decision was silently dropped: ParseOutput only ever extracted
additionalContext from hook stdout, and PermissionRequest's exit code 2 was
explicitly downgraded to a notification instead of a block. A Claude plugin
that relies on hooks to refuse dangerous commands or permissions would show
as fully compatible while the tool call still ran.
- honor Claude's JSON deny decision (hookSpecificOutput.permissionDecision
for PreToolUse, hookSpecificOutput.decision.behavior for PermissionRequest)
even on exit 0, and have exit code 2 block an imported PermissionRequest
hook the same way Claude's own contract does. Scoped to PayloadFormat
"claude" so native Reasonix hook semantics (PermissionRequest stays
advisory-only) are unchanged.
- wire the PermissionRequest hook synchronously into the approval flow so a
deny preempts the prompt instead of only notifying after the fact.
- fix hook dedup (appendUniqueHook) to also compare Env, Timeout, Async, and
Cwd, so two hooks that share a command/matcher but differ in one of those
no longer collapse into one.
- fix Windows path-separator mismatch: ${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PROJECT_DIR}
expansion left a mixed "C:\...\pkg/bin/server" string since the manifest's
"/"-authored suffix was never converted to the host separator.
- lint: tagged switch (QF1003) and remove two functions orphaned by this PR's
own refactor (appendClaudeSettingsHooks, pluginNameExists).
Co-authored-by: SivanCola <[email protected]>
Increase inter-tab spacing, give each themed tab a full border outline, and
render a short divider between adjacent AppChrome tabs so separate sessions are
easy to scan. The active tab also gets a stronger border-color and heavier
label weight. Drag-and-drop placement indicators still override the static
divider when reordering.
The active-tab accent underline is owned by #6416 (merged); this change composes
with it (keeps its project-accent box-shadow, adds the border/weight) rather than
redefining it.
Two P1s from review: a failed/cancelled turn's Reset wiped the per-turn
ledger's background lease, but nothing re-enumerated the job manager's
still-uncommitted evidence for the next turn, so a turn that answered
without calling wait/bash_output again shipped a background mutation
final-readiness never saw — including after a process restart. Run now
re-leases a session's pending background evidence at the start of every
turn via the new PendingEvidenceJobIDsForSession.
Separately, collectBackgroundEvidence recorded NoteBackgroundLease before
confirming LeaseEvidenceForSession had terminal evidence. kill_shell flips
a job's status to Killed synchronously, well before its cancelled
goroutine actually exits and flushes PublishEvidence, so a bash_output
poll landing in that window noted an empty lease and permanently deduped
away every later retry in the same turn. TryLeaseEvidenceForSession now
reports readiness explicitly, and the lease is only noted once ready.
Regression coverage: a same-process second Run without wait, a simulated
process restart, a deterministic kill/unwind race via channels, plus unit
tests for the two new Manager methods.
Review follow-up on the --dir workspace-root change:
- Replace the silent os.Getwd() fallback in runAgent/chatREPL with a
workspaceRootForDir helper that returns an error, so a Getwd failure exits
instead of silently reverting to git-root/default and breaking the explicit
--dir guarantee.
- Add regression tests: resolveWorkspaceRoot honors an explicit dir inside a git
repo and falls back to the git root when empty; workspaceRootForDir returns the
post-chdir working directory (empty for no --dir).
Co-authored-by: 司空祎宸 <[email protected]>
The --dir flag only changed the working directory (os.Chdir) but did not
pass its value as the workspace root to boot.Build. This meant resolveWorkspaceRoot
still walked upward to find the nearest .git, overriding the user's intent.
Changes:
- setup/setupQuiet: accept workspaceRoot string, forward to boot.Options
- runAgent/chatREPL: compute absolute workspaceRoot from --dir after chdirTo
- runServe, m.buildController: pass empty/default workspaceRoot to match new signature
Follow-up hardening for the #6432 reverse race. The snapshot-freshness
guard compared a runtime snapshot's fetch time against the arrival time
of the last approval/ask event. Because that arrival time was refreshed
by EVERY approval_request/ask_request, a delayed replay of an
already-answered prompt (the backend snapshots the pending prompt at
replay-call time and the event crosses the Wails bridge after the user
answers) pushed the anchor forward. An authoritative idle ListTabs
result fetched right after the answer then looked stale and was
discarded, and turn_done's keepPlanApproval resurrected the resolved
exit_plan_mode card — a zombie prompt.
Anchor promptArrivedAt to the prompt id instead: a replay carrying the
same id (post-answer delayed delivery, or the #6429 re-arm after
activation) keeps the original arrival time, so any snapshot taken after
the answer is newer than the anchor and reconciles the dead prompt. Only
a genuinely new prompt id (backend ids are monotonic within a
controller) re-anchors the time, preserving the #6429 stale-snapshot
rejection for the new prompt. The anchor is dropped at hard epoch
boundaries (new user turn, unsend, cancel, tab activation, reset) so it
cannot leak a stale id across turns or controllers.
This keeps approve-failure recovery intact: an optimistic clear followed
by a same-id replay still re-arms the prompt (only the timestamp is left
untouched), so a prompt the backend still holds stays answerable.
Reducer regression tests add the reverse race in both orderings
(idle-before-turn_done and turn_done-before-idle), the new-prompt
re-anchor, approve-failure recovery, and the epoch-boundary anchor
drops. Mutation check: reverting the id anchor to the per-replay refresh
fails 5 assertions, including the two the review flagged (authoritative
idle discarded, resolved plan approval resurrected).
Two layering fixes for the active-tab accent underline:
- The macOS .app--darwin .app-chrome--tabs .tabbar__tab--active top-edge
accent (inset 0 2px) never rendered: applyTheme always sets
data-theme-style, so the later :root[data-theme-style] themed rule (and the
grouped darwin rule) win and paint a bottom-edge accent on every platform.
Revert that dead top-edge declaration to its base box-shadow; the accent
stays a uniform bottom-edge marker owned by the themed layer.
- :root[data-theme-style] .tabbar__tab:focus-visible (higher specificity)
replaced the whole box-shadow and dropped the active underline, so a
keyboard-focused active tab lost its selected marker. Add
.tabbar__tab--active:focus-visible composing the focus ring, graphite
shadow, and project-accent underline; non-active focused tabs keep just the
ring.
Add app-chrome-tabs contract coverage for the active underline, focus
preservation, and absence of the dead macOS top-edge accent.
Two correctness fixes on background mutation-evidence recovery.
P1 — evidence was permanently consumed at collection time, before the
collecting turn committed. wait/bash_output set evidenceTaken, cleared
the in-memory copy, and (since the prior commit) drained the persisted
summary — but the receipts only reach the turn ledger afterwards, and
that ledger is reset on the next user turn. So wait -> collect -> the
turn is cancelled / the provider errors / the process exits -> next turn
lost the mutation from both memory and disk, and the background change
shipped unreviewed.
Collection is now a non-consuming lease. wait/bash_output merge a copy
into the turn ledger and note the lease on it; the agent commits the
leased jobs (clearing memory and draining the persisted summary) only
after the turn reaches a final answer past its delivery gates. A failed
or cancelled turn leaves the lease uncommitted, so the next turn — or a
restart, since the disk summary survives — re-collects it. A committed
job returns empty so a re-poll after delivery does not re-demand review.
Within a turn the lease note dedupes repeat collection.
P2 — a pre-feature artifact (mutationEvidenceVersion 0) recovered as no
evidence, silently skipping review for a legacy background writer task
collected after upgrade. A missing summary only proves the mutation
state was not recorded, not that the task made no changes, so version 0
now recovers as an opaque RiskHigh mutation like any other unparseable
version. Same-version artifacts with no summary still recover as no
evidence — that build did record the state and found none.
go test ./... passes; scripts/cache-guard.sh passes; race-tested the
lease/commit and reservation paths.
Address review of #6424: fallbackCopyText ran document.execCommand("copy")
in a try/finally with no catch. Some WebViews reject the call with
NotAllowedError instead of returning false, so the exception propagated out
of fallbackCopyText, through writeClipboardText's unguarded final return, and
rejected its Promise<boolean>. In the crash overlay's Copy handler that
rejected the await before it could restore the button — leaving it disabled,
the exact #6388 unresponsive symptom. The same throw also surfaced as an
unhandled rejection at the `void writeClipboardText(...)` transcript call site
and synchronously at Composer's direct fallbackCopyText call.
- Root fix at the owner: fallbackCopyText now catches the execCommand throw and
returns false, so writeClipboardText honors its Promise<boolean> contract for
every caller and the hidden textarea is still cleaned up via finally.
- Defense-in-depth on the last-resort surface: the overlay Copy handler wraps
the write in try/catch/finally so the button always re-enables and no
rejection escapes into the global crash handler.
Regression: writeClipboardText resolves false (never rejects) when execCommand
throws, and still tears down the textarea.
Address review of #6424:
- Absorb the focus-resume mechanism from #6138: shouldRecordEventLoopLagSample
now requires both the visibility-resume and focus-resume grace windows, and
focus/blur share visibilitychange's resetSamples so visibleSince/focusedSince
park at +Infinity while hidden/blurred. The sampler tick also self-observes
hidden/unfocused state, so the fix holds when a throttled tick runs before the
transition task is delivered (the actual race), not only when the listener wins.
- Drop the absolute 30s lag cap. Suppression is driven entirely by
visibility/focus state, never by lag magnitude, so a genuine multi-second
main-thread freeze while visible, focused, and settled still reports.
- Route the popup Copy button through the existing writeClipboardText helper
(async Clipboard API -> window.runtime.ClipboardSetText -> execCommand),
the three-tier approach from #5745, instead of a private execCommand-only path.
Regression coverage exercises hidden->resume and blur->refocus with the
transition task never delivered, a 45s settled-state freeze that must still
prompt, and the native-bridge / execCommand clipboard fallbacks.
Co-authored-by: GTC2080 <[email protected]>
Co-authored-by: ttmouse <[email protected]>
The advertised Y/A/P/N/1-5/Esc approval keys were dead in the default UI
state. isPlainKey rejects key events whose target is a text entry, but the
composer is autofocused, so after an approval_request every shortcut typed
into the draft instead of resolving the prompt (pressing Y wrote "y" and left
the approval pending).
Move focus onto the approval card (a tabindex=-1 container, not the Allow
button, so Enter can't approve by accident) when it appears — but only when
the user is not mid-entry: a non-empty composer draft or any other focused
text field (e.g. session search) keeps its focus so a real keystroke is never
turned into an unintended approval. Cleanup returns focus to the composer only
when focus still sits inside the removed card. Consumed shortcuts now
preventDefault so the resolving key does not land in the refocused composer as
a stray character.
Verified with a headless-Chromium behavioral test driving the extracted real
handlers (trusted CDP keystrokes, real focus state): default-state Y/N/Esc
resolve and restore composer focus with an untouched draft; a non-empty draft
or focused search field never resolves and keeps typing; modifier chords are
ignored; turn-end cleanup restores focus and deregisters the listener.
Two follow-ups on background mutation-evidence recovery:
- A pre-feature task artifact (mutationEvidenceVersion 0) no longer
recovers as a synthetic opaque high-risk mutation. That flow never
published child receipts, so there is nothing to recover — synthesizing
one retroactively demanded inspection and review for work no collecting
turn ever saw, pure burden with no safety gain over the old behavior.
An unknown NEWER version still recovers as an opaque high-risk mutation
so downgrade coexistence on a shared state directory cannot skip review.
- TakeEvidenceForSession now rewrites the artifact meta after a
successful take, draining the persisted mutation summary. Previously
only the in-process evidenceTaken flag guarded duplication, so every
restart resurrected already-collected evidence for a re-polled job id
and re-armed review demands for work the collecting turn had already
handled. The rewrite is best-effort under the same job lock the
completion-time meta write already uses.
Switching to a session whose plan approval (or ask) was pending could
flash the prompt and then lose it, leaving only the composer while the
sidebar still showed "waiting confirmation" (#6429, #5561, #5481).
The backend replays the pending prompt event when a detached session
runtime re-attaches during activation, but a runtime snapshot fetched
BEFORE that event (pre-attach ListTabs, activation-time tab metas) can
be dispatched AFTER it. Such a snapshot reports the tab idle with
pendingPrompt=false, so backend_status reconciliation cleared the
approval/ask from frontend state and also skipped the compensating
replay, which only fires when the snapshot itself carries
pendingPrompt=true. The prompt stayed lost until another sidebar click
issued a fresh replay after all wipes.
Fix: stamp every approval/ask event with a monotonic arrival time and
every backend_status dispatch with the fetch-initiation time of its
snapshot. The reducer ignores snapshots that predate the live prompt,
and dispatchRuntimeStatusForTab reports a surviving prompt as
foreground-running so reconcileTabRuntime does not misread the stale
snapshot as a missed turn_done and reset the session out from under
the prompt. Snapshots fetched after the event still reconcile normally,
so answered/dead prompts are cleared as before and prompt-free tabs are
untouched.
Regression tests cover the reducer invariants (stale/tie/fresh/legacy
snapshots, approval and ask, replay-refreshed arrival time), the
hook-level race (approval delivered while a ListTabs fetch is in
flight), and a source-level check that every dispatchRuntimeStatusForTab
call site passes its snapshot fetch time. The suite fails 11 assertions
with the reducer guard disabled and passes with it enabled.
Fixes#6429
A planning turn may legitimately wait on background jobs, but merging a
finished writer task's mutation receipts into that turn's ledger arms
delivery sign-off demands (complete_step, verification, structured
review) that a read-only planning turn cannot satisfy, wedging it into
the readiness retry loop.
collectBackgroundEvidence now skips without consuming while plan mode is
active: the job keeps its published evidence and the first collection
from a normal turn merges it. The plan-mode flag reaches the leaf
builtin package through a new planmode context key mirrored by the
agent's call-context constructor (importing internal/agent from builtin
would cycle through the agent package's own tests), with a regression
test pinning the two flags in lockstep.
Four defects found by adversarial re-review of the previous two commits:
- Cancelling a turn (composer Esc, stop button) stranded pending
approval/ask cards: still rendered, buttons posting to a dead id, and
the approval key listener attached until an unrelated later keypress
resolved it invisibly. Prompts now register a cleanup and
turn_started/turn_done clear them — a turn's prompts die with the
turn. This also fixes the pre-existing stop-button strand.
- scrollDown(true) could be swallowed when a user scroll event landed
between the call and its rAF (scroll steps run before rAF callbacks),
leaving an approval off-screen with a live key handler. Forced scrolls
now re-assert the pin inside the rAF and always scroll.
- During fast streaming (a chunk per frame) every frame re-bottomed the
viewport, so a slow trackpad drag could never accumulate the 40px
unpin distance. Detach now also triggers on wheel intent: any upward
notch on scrollable content unpins immediately.
- The rewind picker shared keys with the composer instead of owning
them: letters typed into the hidden draft and Enter both sent the
draft and advanced the picker (next scope key rewound a running
turn). The picker now runs in the capture phase and swallows the
plain keys it owns; modifier chords, IME composition, and
non-printable keys pass through.
showNotice is direct feedback to a user click and now always reveals.
Carry background task receipts through the job manager and merge them when the parent collects the result, keeping delivery verification and review gates effective across turns.
Restore conservative high-risk handling for opaque writes, serialize evidence-collecting tools, and make the per-session background task cap atomic.
The approval key handler listened document-wide with no target or
modifier check: with the autofocused composer, typing y/a/p/n/1-5 into
the input while an approval was pending resolved it invisibly, and
Cmd+A (select all) could grant a session-wide allow. Gate approval
keys and the '/' focus shortcut on isPlainKey (no Ctrl/Cmd/Alt, target
not an input/textarea/select/contenteditable — '/' typed into the
session search no longer steals focus), and make the rewind picker
ignore modifier chords so Cmd+C cannot hit a scope key and apply a
rewind. The picker still owns plain keys regardless of focus because
double-Esc opens it from the composer.
Approval and AskUserQuestion cards are blocking action requests, not
stream content: with pin-aware autoscroll they could render off-screen
while the user reads history, leaving the session looking hung. Worse,
the approval key handler is document-level, so keystrokes typed into
the composer could resolve an approval the user never saw. Force
re-pin (scrollDown(true)) so these prompts always enter the viewport.
The web UI unconditionally scrolled the transcript to the bottom on every
streamed text/reasoning chunk, so scrolling up to read earlier output was
impossible while a response was streaming — the next chunk immediately
yanked the viewport back down.
Track a pinned-to-bottom state from the transcript's scroll position
(within 40px of the bottom counts as pinned). Streamed content only
auto-follows while pinned; scrolling up detaches, and returning to the
bottom re-pins. Sending a message or loading session history force-re-pins,
matching user intent. Programmatic scrolls are now instant instead of
CSS-smooth: the smooth animation both fought wheel input and reported
intermediate positions that would break pin detection.
Verified in headless Chromium against the exact code block from this page:
follow-while-pinned, detach-on-scroll-up, stay-put-while-detached,
re-pin-at-bottom, resume-following, and force-re-pin all pass.
Setting the sentinel webhook previously required local Cloudflare
credentials (wrangler login / dashboard). The deploy workflow already
holds CLOUDFLARE_API_TOKEN, so mirror the GitHub repo secret into the
worker after each deploy instead; when the repo secret is unset the
step is a no-op and sentinel alerts stay log-only.
The event-loop lag sampler trusted visibleSince whenever a tick observed a
visible view, but WebKit can run overdue timer callbacks before the queued
visibilitychange task when the view is shown again. The first tick after
resume then attributed the whole hidden-view throttling delay to the
previous visible period and painted a performance.lag popup (#6419, #5909).
The sampler now tracks hidden observations itself, restarts the visible
window on the first visible tick, and drops suspension-scale (>=30s)
readings outright.
The popup Copy button was fire-and-forget and could fail silently in
WebViews (#6388): it now awaits the clipboard write, falls back to
execCommand, and reports copied/failed states. A failed Send re-enables
with the failure reason as tooltip so it can be retried.
A delivery-mode background sub-agent that finished its edits but could
not complete the sign-off protocol hard-failed the whole job, discarded
its answer, and pushed the parent into spawning repair tasks for work
that had already landed. A user asking for one content change watched
the model open nine background "fix" tasks that all died on
"final-answer readiness failed".
Four coordinated repairs:
- Salvage readiness-exhausted sub-agent runs as explicitly unverified
answers when a real successful mutation landed. Unbacked "done"
claims (no mutation) and report-required review sub-agents keep
failing. Child receipts still merge into the parent ledger, so the
parent's own delivery gates keep requiring verification and review.
- Defer structured review (review_report / security_review) to the
parent inside sub-agents: the child may not even hold the review
tools, and its mutations already merge upward for parent review.
The light post-mutation review (read the file / git diff) still
applies inside sub-agents.
- Classify path-less (opaque) mutations from ordinary tools as medium
risk instead of high: a bash content edit should demand a review, not
the full security_review protocol. Privileged/opaque tools, sensitive
paths, and 10+ file changes still classify high.
- Cap concurrent background writer tasks at 3 per session: they all
mutate the same workspace with no isolation, and unbounded fan-out is
the fuel of the repair-task cascade. Foreground tasks are uncapped.
go test ./... passes; scripts/cache-guard.sh passes.
The 2026-07-03 D1 size-cap outage went unnoticed for ten days: clients
swallow ping failures by design, ingest handlers surfaced raw worker
exceptions, and nothing watched the write path.
- Wrap the D1 writes in /v1/ping, /v1/metrics and /v1/report so a
storage failure returns a deliberate 503 with a contextual log
instead of an opaque 500 (Cloudflare error 1101).
- Add a sentinel cron (4x/day, hours with guaranteed traffic) that
probes a canary write into pings (removed in the same run) and
today's real ping count, covering both write-path failures and
upstream ingest blackouts. Alerts post to the optional ALERT_WEBHOOK
secret (Feishu/Lark native payload, Slack-style {text} otherwise) and
always land in the worker logs.
The reasonix-crash D1 database grew until it hit the size cap on
2026-07-03; from then on every ingest write (/v1/ping, /v1/metrics,
/v1/report) threw and the /stats dashboard flatlined while reads kept
working. Add a scheduled trigger that deletes time-series rows past the
dashboard's query horizon so the database plateaus:
- pings, metric_users: keep 30 days (queries read at most -29 day)
- metrics: keep 60 days (the 30d view's previous-window delta reads
back to -59 day)
- reports/groups: untouched — triage queue and regression baseline,
growth already bounded by per-group sampling
Deletes run in 10k-row rowid chunks with a per-run backstop so a run
never holds one giant transaction but still drains a large backlog
after an ingest outage.
Use box-shadow inset instead of ::after pseudo-elements to avoid
conflicting with .tabbar__tab--drop-after::after drag-drop indicator.
The accent bar colour respects per-project accent colours via
--project-accent fallback chain.
Fixes#6391
Cache-impact: none — pure CSS.
Build: pnpm --dir desktop/frontend build passes.
jest/vitest --outputFile (and vitest's dotted --outputFile.json form)
write result files but were classified as non-mutating verification
through the npm/yarn wrappers, and every mypy --<type>-report flag
(txt, html, xml, cobertura-xml, ...) writes a report directory while
only --junit-xml was covered.
Add outputfile and pytest-reportlog's report-log to the global
write-output flags with a dotted-form fallback, and give mypy a
runner-specific check that rejects any --*-report flag so future mypy
report types stay covered without denylist updates. Read-only flags
(npm test -- --json, mypy --strict) remain verification.
go test flags accept an optional test. prefix that the go tool passes
through to the test binary, so -test.cpuprofile=cpu.out bypassed both
the global write-output flags and the go-branch check while still
writing the profile. Go 1.26 also has explicit output channels the
classifier missed: -artifacts saves test artifacts under -outputdir,
and -test.testlogfile/-test.gocoverdir write log and coverage files
(-args -test.testlogfile=<path> creates the file even on success).
Strip the optional test. prefix in both matchers, add
testlogfile/gocoverdir to the global write-output flags for wrapper
passthrough, and extend the go-branch list with artifacts plus the
profile family so it stays self-contained. Read-only test.-prefixed
flags (-test.v, -test.run) remain verification.
go test -blockprofile/-mutexprofile write profiles, -trace writes an
execution trace, and -c/-o emit the test binary; all were still
classified as non-mutating verification. Add blockprofile/mutexprofile
to the global write-output flags and handle -c/-o/-trace in the go
branch only, because the dash-stripped global match would also hit
node -c (a syntax-only check) and pytest --trace (a read-only debugger
flag). Add mutation regressions for each flag and verification
regressions for the read-only look-alikes.
Trusted test runners were classified as non-mutating verification even
when invoked with flags that write files: pytest/syrupy
--snapshot-update rewrites checked-in snapshot fixtures (the same class
as the globally rejected --update/-u/--fix), and --junitxml/--junit-xml,
gotestsum --junitfile/--jsonfile, and go test
-coverprofile/-cpuprofile/-memprofile write explicit report or profile
paths. Files produced by such runs bypassed the delivery
re-review/re-verify/sign-off gate because the receipts carried
Mutation=false.
Add a write-output flag guard next to the existing --fix/--update guard,
matching whole flag names in both --flag=value and space-separated
forms. Plain runner invocations remain verification commands.
--test-update-snapshots rewrites checked-in snapshot fixtures (the same
class as the globally rejected --update/-u/--fix) and
--test-reporter-destination can write arbitrary paths. Both were
classified as non-mutating verification, so files they produce would
bypass the delivery re-review/re-verify/sign-off gate. Fail closed on
these flags while keeping plain node --test as verification, and add
mutation and verification regressions for both argument forms.
Node CLI flags are case-sensitive: -c/--check is the syntax-only check
mode, while -C/--conditions executes the target script with custom
export conditions. The classifier lowercased the first flag, so
'node -C production server.js' was accepted as a delivery verification
and stopped counting as a mutation. Match flags exactly and add a
regression test proving -C stays an opaque mutation.
The desktop app hosts bot bridges in the GUI process; the project
search rg subprocess was the one remaining child in that path spawned
without the hidden-window flags, flashing a console on Windows.
Real Claude marketplaces spell relative plugin sources both as
./plugins/example and as the bare plugins/example; the preflight now
accepts both (absolute and drive-qualified paths are still rejected
before the join, and escape attempts still fail the plan loudly).
Entries the subset cannot install — object sources, external URLs, and
invalid plugin names — no longer fail the whole marketplace plan: bulk
installs skip them with a per-entry warning, while selecting such an
entry by name reports a precise error. Names are validated at plan time
so broken entries surface in the preview instead of mid-apply.
Also pins the planId approval contract for marketplaces: the preview
planId must match the planId recomputed by the apply call.
Plan and install relative-path Claude marketplace entries from one pinned GitHub snapshot. Route plugin Git subprocesses through the Windows hidden-window helper and cover marketplace selection, validation, cleanup, and real install behavior.
Two-layer defense against the archive double-click race that caused
'topic not found' errors, rendering loops, and session corruption:
1. Frontend (ProjectTree.tsx): add trashingRef guard — subsequent
clicks are silently ignored while a trash operation is in flight,
using the same pattern as creatingRef for topic creation.
2. Backend (tabs.go): make deleteTopic idempotent — when the topic
is already removed (e.g. by a concurrent trash), return nil
instead of an error. The desired end state is already achieved.
Closes#6312
Place the opaque backdrop on the Creation scrollbar selector so it wins the light-theme background cascade while preserving the custom scrollbar. Tighten the regression test to assert the effective rule block.
The light themes already paint the creation transcript with an opaque
canvas gradient at higher specificity, so the added
--creation-canvas-color never took effect there; the WebView2
stale-pixel ghosting (#6359) only reproduces where the transcript was
transparent, i.e. dark themes. Use the matching opaque var(--bg)
directly and remove the unused variable definitions. Tighten the
regression test to assert the effective rule block plus the dark
canvas invariant it relies on.
Add comments in styles.css and Composer.tsx clarifying that .context-ring-popover
is portaled to <body> (no .app--creation prefix) yet only rendered in creation
layout, and that its font sizes intentionally use calc(px * var(--font-scale)).
Prevents future edits from reverting to bare px or assuming the rule is global.
The context-window ring hover popover is portaled to <body> and is not
prefixed with .app--creation, so it was missed by the creation font-scale
sweep and kept hardcoded px sizes. Scale its font sizes with --font-scale
for parity, and add contract assertions so the portaled popover cannot
regress to bare px.
Creation CSS hardcoded many font-size px values, so settings text size
had no effect while classic/workbench scaled via --font-scale.
Wrap those sizes in calc(Npx * var(--font-scale)) so default stays
identical (scale=1) and small/large presets work. Keep font-size:0
hide-text tricks. Add contract tests for assistant body and composer.
Fixes#5142
Commit 19d580ccd changed .composer-card--resized .composer__input from
height: 100% to height: auto. JS applies no inline textarea height in
resized mode, so the rows=1 textarea collapsed to a single visible line
regardless of the card height the user chose, and multiline input
scrolled inside that one line. Any user with a persisted composerHeight
(anyone who ever dragged the composer resize handle) hit this on every
launch of the latest release.
Restore height: 100% so the input fills the user's fixed card height and
scrolls internally, keep the resized rich input on the same
fill-and-scroll contract, and add a CSS contract test locking both
declarations.
The staged setup manager only added the provider, access entry, and
credential; a first run that configured a custom provider kept the
built-in default_model (deepseek-flash) whose key was never entered, so
boot still failed with a missing DEEPSEEK_API_KEY. The old wizard
pointed default_model at the user's chosen provider.
- After adding providers, promote the first usable new provider to
default_model when the current default cannot run (unresolvable, or
its key neither stored nor staged). A usable default is never moved.
- When an edit/model refresh drops the exact model a "provider/model"
default ref names, re-point the ref at the provider's own default,
mirroring the repair RemoveProvider already performs.
Both paths go through setDefaultModel so the change replays under the
commit-time conflict check.
c146456d 添加的宽字符重绘修复(tea.ClearScreen)仅覆盖了
chatTUI.update() 底部的主输入路径。chooser 的 "Type something"
自由文本模式有自己的输入处理分支,该分支提前返回,完全绕过了
这一检查。
在 typing 分支中捕获 beforeInput,并应用相同的
shouldClearWideInputChange/tea.ClearScreen 逻辑,从而使输入到
自定义 Ask 答案中的中文、日文及其他宽字符也能在 Windows 上触发
完整的终端重绘。
修复 https://github.com/esengine/DeepSeek-Reasonix/issues/6368
│ fix(cli): repaint wide-character input in chooser free-text mode on Windows
│
│ The wide-character repaint fix (tea.ClearScreen) added in c146456d only
│ covered the normal input path at the bottom of chatTUI.update(). The
│ chooser's "Type something" free-text mode has its own input-handling
│ branch that returns early, bypassing that check entirely.
│
│ Capture beforeInput in the typing branch and apply the same
│ shouldClearWideInputChange/tea.ClearScreen logic there, so Chinese,
│ Japanese, and other wide characters typed into a custom Ask answer
│ also trigger a full terminal repaint on Windows.
│
│ Fixes#6368
Keep the Creation workspace dock polish while integrating tab-scoped file APIs, workspaceScopeKey request isolation, and the latest regression coverage.
Co-authored-by: SivanCola <[email protected]>
Second review round:
- The desktop topic-title regression test built its message from a
paraphrased delivery-runtime marker, which the byte-exact suffix cut
correctly refuses to strip — the desktop CI job failed on it. The marker
constant is now exported (agent.DeliveryRuntimeMarker) as the single
source of truth; the test builds from it, and the exact-match contract
stays intact. Its text is byte-identical since introduction, so the exact
cut is safe for all persisted sessions.
- parallel_tasks children now pass the pristine task through
Options.ClassifierTaskText before workspace wrapping, matching
runSubSession. Without it the wrapped text hit the IsTask length fallback
and a conversational child exhausted final-answer readiness instead of
answering; regression test added (fails on the previous code).
Review follow-up for the three P2 regressions:
- Delivery intent classification no longer pattern-strips host framing from
the raw input (user input dressed up as <workspace-context> could disarm
the gates). Sub-agent spawners now pass the pristine task text through
Options.ClassifierTaskText — a host-trusted channel captured before any
wrapping — and top-level turns are classified verbatim, restoring the
pre-PR trust boundary. Spoof-resistance regression test added.
- The trailing <delivery-runtime> strip is a byte-exact CutSuffix of the
host marker instead of a lazy $-anchored regex, which could swallow user
prose between a literal marker mention and the real suffix. Inline-mention
preservation test added.
- Partial tool dispatches without a call ID (OpenAI-compatible streams can
surface the name first) no longer create a synthetic-id card that the full
dispatch can never merge with; arg progress still feeds the composer pill
and the card appears once the real ID arrives. Orphan-card regression test
added.
rangeIntersectsMessage still called Range.intersectsNode directly and
silently returned false where the API is missing, while the menu gate
already had the compareBoundaryPoints fallback. Both paths now use the
same rangeIntersectsNode helper, so the fallback story is uniform.
Delivery-mode hardening from a full-workflow field trace (16-min screen
recording + persisted event log of the same session):
- review_report protocol: append an explicit submission contract to review
subagent task prompts, nudge a finished-but-reportless review subagent on
the same session (evidence preserved so the report can cite earlier reads)
up to 2 times before failing, dump the failed transcript for diagnosis,
and correct the error text that told the parent to call a tool it does
not have.
- delivery intent classification: classify mutation/work expectations from
the user task text only — host framing (subagent/workspace context blocks,
delivery-runtime marker) contained incidental verbs ("resolve") that
classified every wrapped subagent prompt as a mutation request; read-only
registries can never satisfy a state-change expectation, so it is no
longer armed for them (this deadlocked read_only_task reviews).
- final readiness budget: extend nudges past the base cap (3, up to 6) only
while the model keeps earning new host-observable receipts, so a
converging turn (fix -> verify -> re-review) is not cut off mid-recovery
while a stalled turn still fails fast.
- title/preview hygiene: strip the trailing <delivery-runtime> marker in
StripTransientUserBlocks, share one synthetic-user filter
(agent.SyntheticUserPrefixes / IsUserAuthoredTurn) across session preview,
meta turn counts, and desktop topic auto-titles so host-injected readiness
nudges can no longer become titles ("你是谁? <delivery-run…") or inflate
user-turn counts.
- streaming liveness: providers emit ChunkToolCallArgsDelta progress (per
2KB) while tool-call arguments stream; the agent re-emits throttled
partial ToolDispatch events with ArgChars; the desktop upserts a live
"receiving arguments" tool card instead of dropping partial dispatches,
and the composer pill folds streamed arg chars into its token estimate —
a 30KB write_file body no longer looks like a hang for 80 seconds.
- right-panel freshness: a source-agnostic usageSeq keeps 会话指标/用量分析
refreshing during sub-agent runs; the context gauge no longer collapses
to 0/1M when a mid-turn snapshot races a rebuilt executor.
Verification: go test ./... (root and desktop modules), scripts/
cache-guard.sh (tail_avg 92/95/95 >= 90), desktop/frontend pnpm typecheck +
check:css + test:all, plus new regression tests covering every item above.
Landing inside any message body was not enough: selecting message A and
right-clicking message B still offered Copy, which would copy A's text
rather than what sits under the click. The menu gate now requires the
target's message node to intersect a selection range (intersectsNode,
with a compareBoundaryPoints fallback for DOM implementations without
it), so a selection spanning several messages accepts a right-click on
any of them while single-message selections only answer clicks on their
own message. Adds the select-A/right-click-B and cross-message-span
regression cases.
A message selection outlives clicks elsewhere, so the menu-only gate must
also check where the right-click lands: without it, right-clicking the
project tree or tab bar while a transcript selection survived opened the
transcript Copy menu on top of those surfaces' own context menus.
messageSelectionContextText now additionally requires the event target to
sit inside a message or reasoning body (same selector the selection gate
uses); ⌘C interception is unchanged. Adds the selection-inside/target-
outside regression case.
The Wails shell suppresses the webview's default context menu (main.tsx)
because its Reload/Back/Inspect entries can navigate away from the app,
which also killed the native Copy menu for selected message text — ⌘C
worked but right-click showed nothing.
Mount a document-level TranscriptSelectionMenu that opens the app-drawn
ContextMenu with a Copy item whenever a non-collapsed, non-editable
selection touches a message or reasoning body — the exact gates the ⌘C
interceptor already uses (messageSelectionContextText). Copy writes
through navigator.clipboard, then the Wails ClipboardSetText bridge,
then the execCommand fallback extracted from Composer into lib/clipboard.
The listener stays inert in a plain browser, where the native selection
menu still opens; editable targets keep their native menu.
Production telemetry shows performance-pressure reports dominating crash
triage (~18k in 30 days) while carrying no actionable stacks, and the
cumulative long-task rule fires on routine streaming-render bursts.
- Sample JS stacks with the Self-Profiling API: a rolling 10ms/60s
profiler is stopped when a performance prompt fires, and samples that
landed inside recorded long-task windows (plus the lag spike itself
for event-loop reports) are aggregated into a leaf-frame self-time
top list, reported as "long task top frames (sampled)".
- Serve Document-Policy: js-profiling from the Wails asset server so
Chromium WebViews (WebView2) expose the Profiler API; WebKit ignores
both the header and the API and keeps the unattributed report shape.
- Record longtask culprit attribution, surfacing only cross-context
anomalies (self/window culprits carry no signal).
- Raise the cumulative long-task prompt budget from 1.5s to 3s per 60s
window; extract shouldPromptForLongTasks for direct coverage.
- Mention sampled app function names in the report privacy note.
The sidebar now shows a readiness-paused turn as paused instead of
error, matching the transcript's recoverable presentation; a stale
paused badge yields to thinking once the next turn runs. The delivery
status card partitions outside the turn's process fold like warnings,
so its continue action stays reachable instead of collapsing with the
process items.
Known agent notices (final readiness, empty final answer, executor
handoff, tool budget, loop guard) now carry a wire-stable Code on the
event and history contracts. The desktop localizes notice main copy by
code first, so backend wording edits no longer silently break the
exact-text matching in localizedBackendNoticeText, which stays as the
fallback for codeless events and replayed histories.
shouldOpenTodoPanelByDefault no longer takes todos, so the incomplete and
completed cases exercised the identical call. Keep one assertion naming the
combined contract.
The idle steer fallback now submits steer text verbatim as a provider turn
instead of command-interpreting it, so fixtures that steered
"/unknown-command" against an unreachable provider left the controller
running for the rest of the test. The fixture provider is now a local SSE
stub that completes turns instantly, and the quick-click serialization
test tolerates the busy errors racing actions can legitimately observe
while the steer's turn is in flight — it asserts rebuild serialization,
not that every concurrent action wins the turn.
- Snapshot the active tab's WorkspaceRoot under a.mu before a try run
resolves config; the field is rewritten under the lock by spelling
normalization and session-binding redirects.
- AlwaysHiddenSubagentTools now also reports the background job tools
(wait, bash_output, kill_shell) that every subagent registry strips
unconditionally, so the profile tool picker no longer offers dead
choices.
- Try runs are single-flight and cancellable: they derive from the app
context, register a cancel handle, and a new CancelTrySubagentProfile
binding backs a Cancel button on the settings form.
- SkillView.Body ships only for runAs=subagent skills — inline skills
fold references/ into Body at load time and bloated every
Capabilities/Settings fetch across the JSON bridge.
- Cancelling a run folds queued structured guidance back into the draft
as its slash form instead of dropping the invocation.
- Goal mode blocks entity invocations until a plain-text goal exists;
the first goal message used to swallow them as goal prose.
- Edit-replay keeps a hidden submit prefix (referenced-session context)
on structured messages, matching the plain-text branch.
- Hydrated structured messages restore their invocation badges: reload
resolves the serialized slash-form display, and a conservative
leading-known-token fallback rebuilds the entity segments while prose
slashes, unknown names, and mid-text tokens stay plain text.
- The profile name-collision error no longer claims every collision is
a built-in name.
Review follow-ups on the work-fold rework:
- partitionTurnItems returns ordered segments: process that ran after an
answer or steer opens a new fold instead of being pulled ahead of it, so
the transcript keeps the turn's real timeline. Answers and steers are the
only boundaries — turns without intermediate content keep exactly one
fold, and warn notices stay visible without splitting the fold. The
closing fold carries the turn's work-duration label; earlier folds carry
a counts-only label.
- Switching the work-process-fold preference now applies to folds already
on screen through the preference event bus, clearing per-fold manual
overrides, instead of only affecting newly mounted folds.
Co-authored-by: JesonChou <[email protected]>
Review follow-ups on the steer-loss fix:
- SteerText now skips the transient language blocks withTurnPreferences
prepends (present for Chinese text even in auto mode) and cuts a trailing
delivery-runtime marker, so persisted steers replay as steers instead of
degrading into plain user messages — including sessions written before
this fix.
- The steer fallback no longer routes through runGuarded's deliberately
silent running drop: runGuardedOrPark parks the turn while another is
running or finishing and the FIFO drain delivers it when the window
closes. This closes the loss window between a turn's steer-queue flush
and running=false, and also converts steers with no bound executor into
a regular turn (#6350).
Co-authored-by: JesonChou <[email protected]>
- Refuse subagent profile create/update/delete up front while the
controller has active runtime work: the post-save RefreshSkills
rebuild would be rejected anyway, and a file already written by then
strands the UI (save reports failure, the list never refreshes, and a
create retry hits "already exists"). Mirrors the applyConfigChange
precheck order.
- Silence the rich composer input while an IME is composing: no model
resync and no selection restore (removeAllRanges cancels or commits a
composition mid-word) until compositionend performs one authoritative
sync. Composition tracking uses native listeners so the guard does not
depend on React's composition plugin fallback.
- Hand focus and the caret to the plain textarea when removing the last
invocation entity unmounts the rich input, so the next keystrokes no
longer land on <body>.
- Queue an entity-only submit as guidance (rendered with its slash
form) while a turn is running instead of silently dropping it while
clearing the draft.
- Keep literal "#..." text when dismissing the typed recent-session
picker: Escape and close only dismiss (the slash/@ menu contract),
selecting a session remains the only token-consuming path, the panel
follows the live token, and token-driven flows no longer steal focus
into the panel search box.
Per-turn folding hid every assistant message except the last one behind
the Worked-Ns collapse, conflating the model's answer channel with its
reasoning (#4092). partitionTurnItems now splits by channel: reasoning,
tools, phases, info notices, and compaction fold; every assistant message
with answer text renders outside in order, and mid-turn steers render as
user-side bubbles instead of disappearing into the fold. The fold label
counts what it hides, a fold with nothing outside stays expanded, each
reasoning segment gets its own toggle (#6340), and a display setting can
keep folds expanded after completion (#4233).
Run's deferred clearSteerQueue silently dropped steers still queued when
a turn was cancelled, errored, or paused; persistence only happened at
consume time, so the guidance vanished from both the model's context and
history (#6238). The exit path now drains the queue into the session and
emits the Steer events, Agent.Steer rejects text when no turn is active
so the controller converts it into a regular turn, and the composer only
removes queued guidance on an exact text match instead of eating the
first queued item on any consumed-steer notice.
Pre-v1.11 persistence paths (e.g. the "always allow" permission writer)
full-rendered ./reasonix.toml and pinned the built-in default_model
("deepseek-flash") into project folders. Once the user's [[providers]]
replaced the built-in presets, that stale name resolved to nothing and
every launch from that folder hard-failed with:
unknown model "deepseek-flash" (configured: deepseek-pro); note:
defining [[providers]] replaces the built-in presets, ...
The writer was fixed in 2419ca27, but existing poisoned project files
still cannot boot on current builds.
LoadForRoot now falls back to the user-global default_model when a
project override resolves to no configured provider, and boot surfaces
a warn notice naming the ignored value. The fallback only fires when
the user config explicitly defines default_model, so a project-only
config with a broken ref keeps the actionable boot error. In-memory
only: the project file is never rewritten, and a project override that
does resolve still wins.
TestBuildUnknownModelErrorIsActionable now isolates REASONIX_HOME to
make its project-file-is-the-only-config precondition explicit instead
of depending on the machine's real user config.
Fixes#4218
The token fallback required a digit, but keys are opaque strings — a
sloppy relay reflecting "sk-proj-abcdefghijklmnop" walked straight
through the scrub the threat model promised to stop. Layer the redaction
instead: secrets.Redact first (known key prefixes, Bearer/JWT, KEY=value
forms — digit or not), a credential-context rule for the prose forms
secrets.Redact doesn't cover ("api key: <value>" masks the value
unconditionally), full collapse of masked fragments (including the
head/tail remnants secrets' own mask leaves), then the token fallback
widened from has-a-digit to has-a-digit-or-mixed-case. Single-case
digit-free identifiers (invalid_authentication_token,
INVALID_AUTHENTICATION_TOKEN) stay readable; the residual blind spot —
a long single-case digit-free token with no context word — is named in
the comment.
Auth failure bodies are where servers echo credentials: DeepSeek-style
messages contain the masked tail ("Your api key: ****ae54 is invalid"),
and a sloppy relay can reflect the full key it received. The displayed
turn error can travel further than the user's terminal (bot forwarding,
shared screenshots), so scrub both forms from the reason explainError
appends for 401/403: provider-masked fragments and key-shaped tokens
(16+ [A-Za-z0-9_-] containing a digit) become "****", while digit-free
identifiers like invalid_authentication_token stay readable. Other
statuses keep their reason untouched — their bodies don't carry
credentials, and 400 schema errors legitimately contain long identifiers.
TestStreamAuthError pins a deliberate contract the previous commit broke:
the ambient auth error string must not include response-body content,
because servers echo masked key fragments ("****ae54") in 401 bodies and
Error() flows into logs, status lines, and traces where key material must
never propagate.
Keep AuthError.Body and the explainError surfacing (the display layer
extracts the reason deliberately); drop only the "server said: ..."
suffix from Error(), and repin the contract in both directions:
TestAuthErrorBodyStaysOutOfError asserts the body rides the struct but
stays out of the string.
Sibling of the previous commit: SendWithRetry read the 401/403 response
body but dropped it when building AuthError, so a relay's explanation of
*why* auth failed ("token expired", key not entitled to the model) never
reached the user — the actionable line named the key env to fix, but a
valid-yet-rejected key looked identical to a dead one.
Add AuthError.Body, populate it from the already-read snippet, append it
to AuthError.Error() as "server said: …", and surface it through
explainError's auth branch the same way APIError bodies surface. Output
is byte-identical when the server returned no body.
explainError kept the upstream response body only for the request-shaped
4xx (400/422) and collapsed every other mapped status to its bare
localized category line. Relay gateways (one-api/new-api style) wrap the
real cause -- dead upstream channel, unsupported tools, exhausted quota --
in a 402/429/500/503 JSON body, so exactly the statuses a relay returns
were the ones a user could not diagnose: every request through a broken
relay showed only "Server error (HTTP 500): the provider hit an internal
fault" while other clients displayed the relay's reason.
Drop the 400/422 guard in the (renamed) apiErrorReason so every status
that maps to a localized line appends the body reason, still clamped to
800 runes; statuses without a mapping already pass through
APIError.Error() with the body attached. This deliberately flips the
"429 body must not leak" pin from f920da10 -- that commit scoped body
surfacing to the request-shaped 4xx as a first step, not for privacy
reasons, and the 429/5xx body is as diagnostic as the 400/422 one.
- print Audit snapshot fields instead of the struct in the two new test
fatals: passing the snapshot (it embeds sync.Mutex) to t.Fatalf copies
a lock and fails go vet, which was failing lint and all three platform
test jobs
- composer-goal-toggle: drain the insert request's queued rAF refocus
before focusing the execution-method trigger — the late refocus blurred
the trigger and cancelled the tooltip's show timer, failing the tooltip
assertion
- content menu: the '/' item now disables (with an explanatory hint in
all three locales) while the composer has text — slash commands only
parse as the whole message, so inserting ' /' mid-text was a silent
no-op
- close the content menu, direct session picker, and past-chats state
when a run starts: the popover gates open on !running, so stale open
state made the menu pop back unprompted when the turn finished
DeepSeek thinking-mode sessions can hit tool_calls turns that carry no
reasoning_content on every round (endpoint-conditional; reported on the
official API as well as behind gateways). The per-round warn notice then
floods the transcript with identical cards while adding no signal beyond
the first occurrence (#6259, #6333).
Warn on the first missing-reasoning round only, drop the refuted "is a
gateway dropping the reasoning field?" guess from the text, and move the
impact explanation into the notice detail. SetSession re-arms the dedupe
so a swapped-in conversation still gets its one warning.
Refs #6259#6333#6173
Address the third review round:
- first discovery (P1): action=call on an mcp-server id resolves to a
deferred connect target — permission-named with the server's bare
mcp__<server>__ prefix so server-scoped rules match, ReadOnly=false so
plan mode blocks it and Ask gates prompt before any process spawns —
whose Execute connects and returns the live tool directory; connected
servers list side-effect-free, and the Delivery route block now
instructs exactly that flow for server candidates instead of an
uncallable capability id
- shared host (P1): the resolve and inspect already-connected branches
go through the snapshot-refreshing serverTools helper, so a server
connected by a sibling tab still yields routable mcp-tool entries in
this tab's catalog
- review evidence (P1): receipts record host-observed output length;
bash review evidence now requires non-empty output plus an AST-parsed
content command (cat/head/tail/diff/cmp, git diff/show) whose static
argv names the claimed path exactly — pipelines, redirects, summary
and quiet flags (--stat, --name-only, -q), dynamic expansions, and
substring path hits (path.bak) are all rejected
- stale cache (P2): cached tools of a fingerprint-mismatched server
inherit StatusStale so routing prompts expose the outdated schema
End-to-end coverage runs against a real stdio MCP helper: no-cache
connect-list-call, cross-tab shared-host snapshots via resolve and
inspect, plan-mode blocking of the connect target, and seven
content-evidence bypass shapes.
Provider-visible schemas and tool ordering remain unchanged; on-demand
tools stay off the main registry (cache-guard 8/8, tail 92-95%).
Round-3 review follow-up: a converted child that declares its own
$schema is an independent schema resource, but its changed flag still
propagated to every ancestor — so a 2019-09 parent embedding a
converting 2020-12 $defs resource had its untouched declaration
"upgraded", silently changing the semantics of unrelated parent
keywords.
normalizeDraft202012Schema now returns two signals: resourceChanged
(conversions belonging to the caller's resource, stopping at $schema
boundaries) and documentChanged (anything anywhere, driving
reserialization). A resource settles its own declaration update and
returns resourceChanged=false to its parent.
Regressions cover the reviewer's repro (2019-09 parent + converting
2020-12 nested resource keeps the parent dialect) and the contrast case
(a converting subschema without its own $schema still updates the
parent's legacy declaration).
Round-2 review follow-ups on the tuple normalizer:
- The process-global sync.Map memoized externally supplied MCP schema
bytes as both key and value with no eviction, so a long-lived desktop
process accumulated every distinct schema across projects, sessions,
and server reconnects. The memo is gone; the lexical "items" gate
keeps the common no-op case at zero parses, unchanged schemas still
return their input bytes, and the rare legacy-tuple schema is cheap
to re-convert per request.
- Tuple keywords were rewritten before the resource's own $schema was
classified, so an unknown/custom dialect kept its declaration while
items:[...] became prefixItems — a partial conversion under semantics
this code cannot know. Each schema resource's dialect is now
classified first: known legacy drafts convert (and update their
declaration), explicit 2020-12 repairs malformed tuple syntax, and an
unknown dialect leaves the entire resource subtree byte-identical.
Regressions cover the custom dialect at root (byte-identical) and
nested beside a converting sibling, the explicit 2020-12 repair, and
determinism without the cache.
Address the second review round on the Delivery capability runtime:
- canonical names (P0): export plugin.ModelToolName (collision-hash
aware) and use it for every proxy-resolved target; the proxy-local
normalizeToolToken is deleted, so permission/hook deny rules written
for the executed name (e.g. raw tools with @, /, spaces) always match
- delivery routing (P1): RouteDecision carries a Delivery flag; the
transient route block keeps concrete mcp-tool ids and instructs
use_capability(action="call", ...) instead of connect_tool_source,
which is not registered in Delivery
- catalog persistence (P1): the proxy snapshots host-observed live
tools of on-demand connected servers (never touching the registry);
boot and controller pass the snapshot as CatalogOptions.ProxyTools so
mcp-tool entries stay routable after the server turns ready, without
duplicating registry-backed servers
- review evidence (P1): HasReadEvidenceForPath replaces the observed-
path check — only read receipts (exact or fuller-path slash-suffix)
and content-revealing commands (diff/cmp/cat/head/tail, git diff/
show) count; git status/echo mentions, write receipts, and reverse
basename suffix matching are rejected
- windows CI (P1): review_report error text lists reviewed paths in
slash-canonical form so messages and tests match across OSes
- metrics (P2): action=call proxy audit moves to the agent loop's
single post-execution point (no double count, successes now counted);
capabilityGateFailure tracks per-turn misses and records
RequireRecovered/PreferRecovered via the new RecordGateRecovery when
the gate later passes clean
Provider-visible schemas and tool ordering remain unchanged; on-demand
tools still stay off the main registry (cache-guard 8/8 pass, tail hit
rate 92-95%).
Review follow-ups on the MiMo tuple-schema normalizer:
- A schema declaring an older dialect (draft-03..07, 2019-09) kept that
$schema after its tuple keywords were rewritten to 2020-12 form,
making the output self-contradictory — prefixItems under a draft-07
declaration can legally be read with the wrong tuple semantics. The
recursive pass now reports whether it changed anything and updates an
old-draft $schema on every schema resource whose subtree converted
(root and nested $defs resources alike); unknown or custom dialect
URIs stay untouched.
- The normalizer unconditionally re-parsed and re-serialized every tool
schema on every request from both MiMo builders, defeating the
registry's one-time canonicalization for the ~200-tool surfaces that
motivated the PR. Schemas without an "items" keyword now return their
input bytes without a parse, unchanged schemas return their exact
input bytes, and conversions are memoized process-wide (pure function,
bounded by the distinct schemas seen).
The modern-array-items test pinned the old always-reserialize behavior
(key-sorted output); it now asserts the stronger contract that no-op
inputs come back byte-identical.
Address the six review findings on the Delivery capability runtime:
- plan mode (P0): re-run the plan-mode policy after proxy resolution
against the real target's name/read-only/trust/safety, so
use_capability(action=call) can no longer reach a write-capable MCP
tool in plan mode; trusted read-only targets still pass
- permission-before-start (P0): ResolveCall is now side-effect free —
unconnected servers resolve to a deferred target that connects in
Execute, after the permission gate and PreToolUse hooks; inspect lists
live tools or cached schemas and never starts a process
- lifecycle (P0): on-demand connects use AddWithLifecycle(sessionCtx,
handshakeCtx) so the stdio child survives the 5s handshake budget and
exits with the session instead of dying before the tool call runs
- capability gate (P1): finalReadinessCheck now checks the require/
prefer gate before the no-writer early return, so read-only answers
cannot skip a user-required capability
- review_report (P1): reviewed_paths must be backed by host-observed
read/diff receipts in the subagent's own ledger; Delivery review/
security subagents fail their parent call when they finish without a
kind-matching report instead of retrying silently
- unified specs (P1): the proxy consumes boot-converted []plugin.Spec
(env expansion, workspace overrides, timeouts, trusted read-only
tools) instead of hand-copying config fields
- cached tools (P1): Boot and Controller catalog builds now pass
CachedTools/CacheHashOK loaded from the shared spec fingerprints, so
auto_start=false servers surface concrete mcp-tool candidates and
stale caches are marked
- minor: SemanticRouter prices its usage events and records router
token/cost/latency into the capability audit; the doctor illegal
profiles warning is reachable again via Skill.InvalidProfiles
The provider-visible tool schema and ordering are unchanged; the proxy
keeps its stable schema and on-demand tools stay off the main registry
(cache-guard tail hit rate 92-95%, unchanged).
Two review findings on the parked-turn admission change:
1. close() only cleared the parked queue that existed at that instant.
A submit arriving after that critical section — while a still-running
turn's TurnDone delivery was in flight — saw finishing=true, parked
again, and the drain started it against freed resources once the
window closed. Admission is now sealed by a closed flag maintained
under the same c.mu: close() sets it alongside clearing the queue,
runGuarded rejects with a new turnDroppedClosed result (deliberately
silent — the controller's transports are being torn down and the
input's home is the replacement controller the host swaps in), and
the finishGuardedTurn drain refuses to start anything once closed.
2. The synchronous RunTurn gated on running || rotating but not
finishing, so an ACP-style caller could start a turn inside the
previous turn's TurnDone delivery window — recreating exactly the
completion/transport crosstalk the window exists to prevent (and
that Running() already reports as busy). finishing and closed are
now part of the gate; synchronous callers get ErrTurnRunning rather
than parking, since they hold a request/response boundary open and
already handle busy.
Deterministic regressions (window held open by a sink blocking inside
TurnDone delivery): Close-then-submit-then-release must reject at
admission and start nothing when the window closes; RunTurn during the
window and after Close must return ErrTurnRunning promptly. Full
internal/control passes under -race; full root and desktop suites pass
— confirming nothing depended on the previous submit-after-close
behavior (which silently STARTED a turn on a closed controller).
Add explicit list, create, edit, delete, try, and run commands backed by the same profile files and model overrides as Desktop. Keep try read-only and preserve ordinary reasonix run semantics.
The capdiag sanitizer kept its own short list of credential shapes
(Bearer / Authorization= / token= / api_key= / PATH=), so the raw HTTP
response bodies (up to 4KB) and stdio stderr that transport errors embed
could leak JSON credentials ("access_token":"..."), colon-separated
header values, and cookie values into shareable diagnostic reports.
- extend secrets.Redact: keyValuePattern now tolerates JSON-quoted keys,
and Cookie/Set-Cookie header values are masked pair-by-pair; both
changes stay idempotent, preserving session digest stability
- sanitizeErrTextWithPaths reuses secrets.Redact instead of maintaining
a second, narrower recognizer; PATH= and short-Bearer handling stay
- regression tests: HTTP 401 JSON body + Cookie, JWT, GitHub and OpenAI
tokens, colon-separated headers, Set-Cookie
- document the redaction pipeline in CAPABILITY_DIAGNOSTICS (en/zh)
Run manual subagent slash commands in an isolated child loop while retaining only the task and distilled answer in the parent session. Keep custom-path profiles read-only in Settings and reject unsupported or mismatched mutation scopes.
Make static collection truly read-only (LoadForRootReadOnly), harden error
redaction for paths/PATH/tokens, fix Windows pipe capture deadlock, surface
unknown Hook events, use HasTools for no-tools warnings, skip live MCP cache
persistence, and take an atomic desktop tab snapshot with last-request-wins UI.
User-wait for approval and ask now starts at the event on each tab's
controller state, so background suspension is excluded from model work time
instead of only counting once Composer mounts the draft. Composer consumes
turnWaitAccumMs/promptWaitStartedAt and only keeps a local pause for
controller-unknown surfaces (clear-context / legacy tests).
Make single-select Ask confirm follow the keyboard cursor (initial Enter /
ArrowDown+Enter), keep multi-select cursor distinct from checked state, and
scope the composer wait clock by draftKey+turnStartAt so suspended tab
switches do not leak pause intervals.
Round-6 review follow-up: plugin names may legally contain dots, so the
fixed "<target>.pre-replace" backup path could be a sibling plugin's
real install dir — updating "foo" pre-cleaned and then deleted the
installed "foo.pre-replace" plugin while its registration survived.
The backup now derives from the staging directory name: dot-prefixed
and randomized, it can never pass IsValidName, so no valid plugin can
occupy it; the uniqueness also removes the pre-cleanup RemoveAll that
did the damage. Regression installs "foo" and "foo.pre-replace" side by
side and verifies the sibling's files and registration survive a
replace=true update of "foo".
runGuarded treated running, finishing, and rotating as one silent no-op,
but the three states carry different user intent:
- finishing: TurnDone is emitted INSIDE this window (finishGuardedTurn
sets running=false/finishing=true, emits, then clears finishing), so
from every caller's perspective the turn is already over — submitting
again is legitimate. Silently dropping it loses real input: any caller
that submits upon seeing turn_done (a frontend's queued auto-send, a
bot, a fast Enter) races the window. Observed as a flaky lost turn in
CI, reproduced on a clean main-v2 worktree under load, and already
worked around in Composer.tsx (its auto-send is gated on
submitDisabled instead of turn_done precisely because an item was
"removed as if it had sent").
- rotating: the executor session is being swapped; which session the
input was meant for is ambiguous, so guessing is worse than refusing.
- running: interactive frontends already own the steer/queue UX and
internal opportunistic callers (goal-loop continuations, replays)
rely on the quiet no-op.
runGuarded now returns an admissionResult and treats the states
accordingly: finishing parks the body in a FIFO; rotating refuses with
a user-visible warn Notice asking to resend; running stays a silent
no-op. finishGuardedTurn starts the oldest parked turn under the SAME
critical section that clears finishing — opening the gate first would
let an unrelated submit slip ahead and bounce the parked turn back to a
drop. Remaining parked turns drain one per turn end, preserving order.
Rotation cannot interleave with a drain: beginRotation refuses while
running or finishing, and the drain flips finishing directly into
running. close() discards parked turns so one cannot start against a
controller being torn down.
TestRunGuardedRejectsReplacementUntilTurnDoneReturns pinned the old
drop behavior; it is renamed to ...ParksReplacement... and now asserts
both halves of the revised contract: nothing starts inside the window
(the transport-crosstalk guarantee the window exists for), and the
parked turn starts exactly once when the window closes. New
admission_test.go covers FIFO order across multiple parked submits,
the rotating Notice, the unchanged silent running no-op, and
Close-discards-parked. All run clean under -race (10x for the new
tests; full package).
The finishing window itself is unchanged — TurnDone delivery still
completes before any successor turn starts. Longer-term, turn-ID-scoped
sink fan-out could remove the window (and this queue) entirely; left
out deliberately as a larger transport-contract refactor.
Add an on-demand builtin skill and a shared read-only diagnostics model for
Skills, Commands, Hooks, plugin packages, MCP, and instruction docs. CLI
defaults to static probes; --live uses an isolated Host. Desktop Settings
exposes the same report without starting MCP or mutating config.
Root-caused the recurring internal/control timeout flakes (seen twice in
this PR's CI race job at 2s and once in the coverage job after the
deadline was raised to 30s — the full 30s elapsing proved the awaited
event never arrives, not that scheduling was slow):
finishGuardedTurn deliberately emits TurnDone INSIDE the finishing
window (running=false, finishing=true, Emit, then the deferred
finishing=false), and runGuarded silently no-ops while finishing is set
— its documented "no-op if a turn is already in flight" admission rule.
So "received TurnDone" does not mean "may submit the next turn":
- TestSubmitUserTurnBypassesCommandDispatch submitted its second turn
the instant TurnDone arrived; raced into the window, the submit was
silently dropped and the second TurnDone never came (any deadline
times out). Reproduced on a clean origin/main-v2 worktree under
whole-repo test load — pre-existing, not a PR regression.
- TestCancelClearsPending{Approval,Ask}RuntimeStatus asserted an idle
RuntimeStatus immediately after TurnDone; within the window
Running() is still true by design.
Add a waitIdle helper that polls the same running||finishing gate the
controller's admission check reads, and use it at the three sites that
submit-or-assert right after a TurnDone. The eventual-idle contract is
still asserted (waitIdle fails the test if the gate never reopens);
only the false "instantly idle / instantly admissible" assumption is
removed, matching finishGuardedTurn's documented semantics.
Verified: the affected tests pass 30x consecutively, and two full
whole-repo test runs (the load condition that reproduced the hang) are
clean.
Product-side note, deliberately out of scope here: runGuarded dropping
a submit that lands inside the finishing window is silent. A real
frontend submitting immediately on TurnDone could in principle lose a
turn the same way; whether that admission gap should queue-or-retry
instead of dropping deserves its own change with a chat-runtime race
audit.
Round-5 review follow-up: replaceCopiedPlugin deleted the previous
install before copying, and the round-4 capability verification deleted
the fresh copy on mismatch — so a replace=true update to a package that
fails verification destroyed the working version while its registration
survived, leaving an enabled plugin with no root.
installCopiedPlugin now copies into a staging directory beside the
target, runs the capability verification there, and only then swaps the
staged tree into place via a backup-protected rename (same parent dir,
so each rename is atomic; the backup restores on a failed swap and stale
backups from interrupted runs are cleaned first). Any failure before the
swap leaves an existing installation untouched.
Regression installs a valid version, then applies a replace=true update
whose command symlink escapes the package: the update fails, the
previous version's files, version, and registration all survive, and no
staging or backup trees are left behind.
Replace one-click approvals with select-then-confirm decision shelves so
tool approval, plan approval, ask, and clear-context share one footer surface.
Keep the composer mounted under a hidden host to preserve drafts, pause model
work timing while waiting on the user, and surface waiting_confirmation as an
amber pill in the compact project tree.
Second-round review findings on the subagent profiles page:
1. Try-run tools now resolve relative paths against the active tab's
workspace root. The registry was assembled from compile-time builtins
plus Confine* overrides — which bound the sandbox/confinement roots
but never set workDir, so read_file/ls/glob/grep/code_index and
read-only bash resolved relative paths against the desktop process
CWD. In a multi-workspace session a try run could inspect (and send
to the provider) a different project than the one on screen. The
parent set now comes from builtin.Workspace{Dir: root, ...}.Tools() —
the same per-workspace assembly boot uses for desktop tabs — before
the read-only registry is applied. Regression test runs with process
CWD deliberately different from the workspace root and asserts
relative read_file/ls resolve inside the root.
2. editableSubagentProfile now also requires runAs=subagent. A
hand-authored manual-invocation INLINE skill carries only
editor-managed frontmatter keys, so it passed the previous check and
UpdateSubagentProfile would rewrite it with runAs: subagent —
silently converting an inline playbook into an isolated subagent.
3. DeleteSubagentProfile re-resolves its target and applies the full
profile-identity check before deleting. It previously called the
generic DeleteSkill directly, so a stale UI list or a direct bridge
call could remove an unrelated user skill that merely matched
name+scope.
Round-4 review follow-up: discovery (skills and commands) follows
symlinks, but copyDir skipped every symlink — its comment even claimed
otherwise — so a package whose command was an in-tree symlinked alias
planned and reported N capabilities while the installed copy resolved to
fewer, silently.
- copyDir now materializes a symlink as its resolved content only when
it points at a regular file inside the source tree; escaping, broken,
and directory links are still never followed.
- Copy-mode plugin installs re-parse the installed tree and require its
capability counts to match the approved plan; a mismatch removes the
copy and fails the action with a mode=link / fix-the-package hint, so
anything materialization cannot cover fails closed instead of
shrinking the install.
Regressions cover both paths (in-root alias survives; out-of-tree link
refuses and cleans up), skipping on platforms without symlink support.
Repo-wide sweep follow-ups from the subagent-profiles review (same defect
shapes, different sites):
1. internal/memory/store.go render() emitted hand-concatenated
frontmatter, so a memory title or description containing ": ", '#',
or quotes produced unparseable YAML; frontmatter.Split then returned
an empty map and the reloaded memory silently lost its name, title,
and type. Now marshaled via yaml.v3 (indent 2, struct field order),
which renders byte-identically to the old format for plain values —
pinned by TestRenderPlainValuesKeepLegacyBytes — so existing files
rewritten on save do not churn, and metacharacter values round-trip
(TestRenderEscapesYAMLMetacharacters).
2. desktop/memory_suggestions.go had its own string-concatenation skill
file renderer with the same flaw in the free-text description; it now
delegates to skill.RenderSkillFile (yaml-escaped), deleting the
duplicate helper.
3. internal/cli/review.go built its subagent registry from zero-value
(unconfined) reader tools and an empty bash sandbox spec, so
`reasonix review` ignored the user's [sandbox] forbid_read roots and
bash mode. The registry now replaces init-time defaults with confined
instances mirroring boot's addBuiltins (ConfineReaders/ConfineSearch/
ConfineBash from the loaded config), verified by a behavioral test
that a forbid_read path refuses to read through the registry.
4. internal/control test files used 2-second wall-clock deadlines for
"wait for expected event" assertions; under whole-repo parallel test
load these flake — TestSubmitUserTurnBypassesCommandDispatch was
reproduced failing on a clean origin/main-v2 worktree with zero PR
changes, and CI's race job hit it and
TestCancelClearsPendingApprovalRuntimeStatus across two runs. All 60
such sites (verified positive waits — every timeout branch fails the
test; none assert absence) are raised to 30s: correct code completes
in milliseconds, the deadline only bounds a genuine hang, so the
fail-fast property is kept without the scheduling-noise false
positives.
Out of scope, unchanged: internal/skill/skill.go stubBody writes a fixed
literal description with a regex-validated name (no injection surface).
Round-3 review follow-ups on the commands mapping:
- P1: plan and apply each cloned the source separately, so a repo that
moved between the approved plan and the apply resolution could install
content the approval never covered (and the optional planId echo could
not catch the apply call's own second clone). pluginSource now reports
the resolved commit for git sources; the plan records it on the action
— entering the plan ID, so approvals fingerprint the exact snapshot —
and apply pins a diverged clone back to the approved commit via a
full-SHA fetch + detached checkout, refusing with a re-plan error when
the snapshot is no longer reachable. Regression covers the unpinnable
drift refusal end to end.
- P2: the command-dir adoption gate capped recursion at 5 levels while
the runtime loader walks unbounded, so a plugin whose only command sat
deeper was never adopted. The gate now delegates to internal/command
itself — adoption and runtime discovery share one implementation and
cannot diverge; a six-level nesting regression pins it.
The site/ auth client's redirect-validation tests (safeNext) had an npm
test script but no CI job invoked it, so the security tests never ran on
PRs. Add a `site` job that runs `npm test`. The tests use only Node
builtins, so the job needs no npm install.
Address four review findings on the subagent profiles page:
1. Try It is now read-only and workspace-confined. It previously
registered zero-value (unconfined) file tools with no permission
gate, letting a try run write outside the workspace unapproved. It
now resolves config against the active tab's workspace root
(LoadForRoot, not process CWD), replaces init-time tool defaults
with the same confined instances boot's addBuiltins installs
(ConfineWriters/Readers/Search/Bash/WebFetch), builds the registry
via ReadOnlySubagentToolRegistry (writers stripped, bash under the
plan-mode safe policy), and runs under the headless permission gate
so configured deny rules apply. A settings-page preview has no UI to
answer approval prompts, so read-only is the honest posture; the
placeholder copy now says so.
2. Editing is limited to profiles this page can round-trip. The editor
only carries name/description/color/invocation/runAs/model/effort/
allowed-tools; saving a hand-authored skill would silently drop
anything else — most dangerously `read-only`, which boot.go uses to
pick the read-only tool registry, so an edit could turn a read-only
agent writable. It also pre-filled the form from the runtime Body
(references/ + scripts/ expanded), baking that content into the main
file on save. The frontend now lists only invocation=manual profiles
as editable, and UpdateSubagentProfile refuses (server-side) any
skill with unmanaged frontmatter keys or a references/ or scripts/
sibling directory.
3. RenderSkillFile emits frontmatter via yaml.v3 instead of string
concatenation. Free text containing ": " (e.g. "Review code: focus
on security"), '#', quotes, or newlines produced unparseable YAML;
frontmatter.Split then returned an empty map and the loader silently
reset runAs to inline and invocation to auto — dissolving both the
isolation boundary and the no-autodiscovery guarantee. Regression
tests cover colon/hash/quote/newline/list-lookalike inputs.
4. Per-name model/effort overrides are alias-aware end to end. Runtime
dispatch resolves underscore/hyphen key variants (security_review ⇄
security-review); the UI read exact keys only, so a legacy alias
entry displayed as "inherit default", a clear left it silently
active, and effort validation could check the wrong model. boot's
key-alias helper is now exported (SubagentModelKeys) and used by the
view populate, the effort validator, and set/clear, which sweep all
alias entries.
Repo sweep for sibling defects: internal/memory/store.go's render()
hand-builds frontmatter and a colon in a memory title breaks its parse
the same way (pre-existing, different subsystem — follow-up, not
fixed here); `reasonix review` CLI builds unconfined zero-value readers
(pre-existing, read-only posture — follow-up); no other exact-key
SubagentModels/SubagentEfforts lookups remain (task has no alias
variants; other sites iterate existing keys).
The same-origin branch returned u.pathname + u.search + u.hash. Inputs
like /.//evil.example or /a/..//evil.example resolve to a same-origin URL
whose pathname is "//evil.example"; handing that bare pathname to
location.href re-parses it as a protocol-relative URL and navigates to
evil.example. Return the fully-serialized u.href instead — it keeps the
origin attached, so the assignment can never leave reasonix.io.
Added end-to-end regression tests that re-resolve the returned value and
assert it stays on-origin, covering /.//, /a/..//, /..//, and /%2e//.
Review follow-ups on the commands mapping:
- P1: planGitHubPluginPackage assembled a sparse temp dir holding only
the fetched manifest, so convention-discovered capabilities (skills/,
commands/) and their warnings were invisible at plan time while apply
cloned the full repo — the approved plan under-described what actually
got installed. Plan and apply now resolve the source through one
shared pluginSource resolver (shallow clone for GitHub), making the
approval contract hold by construction; a plan/apply consistency
regression covers it.
- P2: dirContainsCommandMd only looked at the top level, so a plugin
whose commands all live in namespace subdirs (commands/git/commit.md)
was never adopted even though the runtime loader walks them. The
adoption gate now recurses (bounded depth, hidden dirs skipped).
- P3: appendInventoryText's empty-inventory fallback ignored commands, so
a commands-only plugin printed its command list followed by
"capabilities: no detailed inventory available".
Verified: reviewer's repro now plans skills=6 commands=11 for
github.com/OthmanAdi/planning-with-files with zero warnings.
The raw-string prefix check in safeNext() could still be bypassed:
URLSearchParams decodes percent-encoding, so ?next=/%09/evil.example
arrives as "/\t/evil.example" — the tab passes the "/" + not-"/"-or-"\\"
prefix check, but a browser strips ASCII tab/CR/LF while parsing the URL
for navigation, collapsing it to "//evil.example", a protocol-relative
redirect off-site. %0A and %0D behave the same way.
Replace the prefix check with new URL(next, origin) and validate the
*parsed* origin/host, which applies the same normalization a browser
applies before navigating, closing this class of encoding bypass rather
than patching one more special case.
Extracted safeNext() into safe-next.js (a pure function with no DOM/
import.meta dependency) so it's unit-testable with node:test, and added
regression coverage for the tab/CR/LF-smuggled case plus the existing
protocol-relative and backslash bypasses. Run via `npm run test` in
site/.
Claude plugins like planning-with-files ship their primary interface as
commands/ (flat <name>.md prompt templates invoked as /<name>), which
Reasonix previously dropped with a "does not map yet" warning — the
plugin installed but /plan, /pwf etc. never appeared, reading to users
as a failed install (discussion #5762).
A Claude command is exactly Reasonix's custom slash command shape
(internal/command: frontmatter description/argument-hint, $ARGUMENTS /
$1..$N substitution, subdir namespacing), so map the capability instead
of warning about it:
- pluginpkg: Manifest gains Commands (explicit "commands" list in any
manifest kind; claude plugins adopt conventional commands/ and
.claude/commands/ automatically, even alongside explicit skills).
Inventory/CapabilityCounts/describe now report commands, loaded via
internal/command so names can never drift from runtime behavior.
- config: CommandDirsForRoot prepends enabled plugin packages' command
roots at the lowest priority — boot and hot ReloadCommands both pick
them up, and a user- or project-authored command wins name clashes.
- cli/installsource: plugin verify checks command roots; install plans
carry commands/commandCount; plugin show lists commands.
- desktop: plugin view exposes commands count + details; capabilities
panel renders a Commands section (en/zh/zh-TW strings).
- docs: EN/zh-CN plugin docs describe the mapping; commands removed
from the unmapped-capabilities warning list (agents, hooks/hooks.json,
.mcp.json still warn).
Verified end-to-end against planning-with-files v3.4.0: installs with
zero warnings, 6 skills + 11 commands, /plan invocable.
safeNext() allowed any value starting with "/", including "//host" and
"/\host" — both are protocol-relative URLs that a browser resolves to
a different origin when assigned to location.href, letting an
attacker craft a post-login redirect off-site (e.g.
?next=//evil.example). Require the second character to not be "/" or
"\" so only true same-origin paths pass; absolute reasonix.io URLs
still go through the existing https + host check below.
Add a "子智能体" (Subagents) settings surface backed by the existing
Skills system: a subagent profile is a skill file with `runAs: subagent`
frontmatter plus a new `invocation: manual` flag that excludes it from
the pinned system-prompt index, so the model never auto-discovers or
invokes a profile the user configured for their own deliberate use
(still invocable explicitly via /<name>).
Backend:
- internal/skill: new `color`/`invocation` frontmatter fields, exported
RenderSkillFile/SkillFileOptions, Store.UpdateContent/Delete, and an
index.go filter that keeps manual-invocation skills out of the
cache-stable Skills index.
- internal/control: Capabilities gains CreateSkill/UpdateSkill/DeleteSkill
so desktop can author skills through the live store.
- internal/agent: export AlwaysHiddenSubagentTools for tool-picker use.
- desktop: new subagents_app.go (Create/Update/Delete/Try + tool
enumeration), SkillView extended with Model/Effort/AllowedTools/Color/
Invocation/Body/ConfiguredModel/ConfiguredEffort, and per-name
SetSubagentProfileModel/Effort setters so built-in rows can override
their model/effort (the only lever available for built-ins, which have
no editable frontmatter file).
- desktop/subagents_app.go: TrySubagentProfile runs a profile once in
full isolation, mirroring internal/cli/review.go's standalone
provider+registry construction — it never touches Controller.RunSkill
or the chat-turn dispatch path.
Frontend:
- New SubagentsPanel.tsx: list page (built-in read-only section + custom
section with edit/delete), create/edit form (name, color swatches,
model/effort, description, tool-scope picker, system prompt, scope,
try-it), lazy-loaded like the existing Skills/MCP pages.
- New "Subagents" tab wired into SettingsPanel.tsx between Skills and
Plugins; ModelPicker/allRefs/toRef/EFFORT_PRESETS exported for reuse.
- i18n additions across en/zh/zh-TW.
Guardrails (from an adversarial design review before implementation):
- Reject a profile name colliding with a built-in subagent name — the
Store's dedup rules would otherwise let a same-named user file
silently shadow the built-in everywhere.
- invocation=manual keeps every profile created here out of the pinned
Skills index (and its shared, capped character budget), closing both
an unwanted-autonomy path and a prefix-bloat path in one field.
Known limitations (deliberately out of scope for this PR):
- /<name> still inlines a subagent skill's body rather than isolating it
(existing behavior in internal/control/input.go); changing that
touches the protected Chat Runtime critical path and is left for a
separate, narrower change.
- The "available tools" picker only lists compile-time builtins, not
MCP/plugin tools (those are per-session/connection).
Cache-impact: internal/skill/index.go's render loop now skips
invocation=manual entries. Existing skills default to invocation="auto"
and render identical bytes to before (verified by
TestApplyIndexMandatesInlineButRestrainsSubagent-style existing
coverage plus new TestManualInvocationSkillExcludedFromIndex). The
pinned prefix only changes when a user actually creates/edits/deletes a
subagent profile through this new UI — the same trigger condition
`install_skill` already has today, not a new source of turn-to-turn
churn.
Rework of the previous two commits, addressing the review blockers:
- Tool output text passes truncateToolOutput (32KB head+tail splice)
before providers see it, so markdown-inlined base64 larger than the
budget was spliced and the extraction regex captured corrupted
payloads across the gap - an API 400 for every real screenshot.
Images now ride outside the text: parseToolResult returns validated
data URLs (mime whitelist, 4MB/5-image budget, base64 check) plus a
short text placeholder, remoteTool implements the new tool.ImageTool
optional interface, and the agent stores the images on the RoleTool
session message untouched by truncation.
- The OpenAI API rejects image_url parts under role "tool"; tool images
are now injected as a user message after the turn's tool-result run.
- Non-vision models (e.g. DeepSeek) no longer receive raw base64 text;
they see only the placeholder, matching the existing user-image gate.
Anthropic keeps the tool_result array-content shape (text + image
blocks); text-only results serialize byte-identical to before, guarded
by regression tests on both providers.
Narrow Creation sidebar/right-dock defaults, keep 文件/改动 tabs self-centered
without overlapping Windows caption controls, align brand logo with the new-chat
icon baseline, and replace the native transcript scrollbar with a proximity-aware
custom rail. Also reopen the right dock into files/changed (never invisible context).
Show session and uncommitted workspace changes before a collapsed commit history in the Creation Changes view, while keeping classic and workbench behavior unchanged.
Align the Creation workspace file tree with the left project-tree language, and reduce topicbar noise by hiding the duplicate Changes control and demoting Command to a utility icon.
parseToolResult converts MCP type:image content to markdown data URIs,
but both the OpenAI and Anthropic providers were sending tool result
content as plain text, so vision models never received proper image
blocks and instead saw raw base64 data.
Now the provider layer detects the markdown image pattern and converts
it to structured image content:
- Anthropic: tool_result.content becomes an array of text + image
source blocks (tool_result supports array content per API spec)
- OpenAI: tool message content becomes an array of text + image_url
content parts
The shared SplitToolResultContent helper extracts markdown images
from tool result text and returns text parts + image metadata.
Refs #6169
parseToolResult only handled MCP content items of type 'text',
silently dropping type 'image' items that carry base64-encoded
image data. This caused MCP tools returning images (e.g.
photoshop_get_preview, take_screenshot) to have their images
removed before reaching the LLM.
Now parseToolResult recognizes type 'image' content and converts
it to a markdown image reference (data URI) so the LLM can see
it.
echo "::warning::npm-v$version is pushed but npm 'latest' is still ${got:-<unreadable>} — the npm publish run is likely awaiting release-environment approval or still propagating. Approve/monitor the release-npm run; its verify step asserts the dist-tag lands."
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."
> **Reasonix 1.0 is a ground-up rewrite in Go** — this branch (`main-v2`) is the new default and where development happens now.
> The earlier `0.x` TypeScript releases are **legacy**, living on the [`v1`](https://github.com/esengine/DeepSeek-Reasonix/tree/v1) branch (maintenance only).
> See the **[migration guide](./docs/MIGRATING.md)**. `npm i -g reasonix` stays the install command — `1.0.0`+ delivers the Go binary, `0.x` is the legacy TS build.
<img alt="Star History Chart" src="https://raw.githubusercontent.com/esengine/DeepSeek-Reasonix/star-history/assets/star-history/star-history-light.svg" />
</picture>
</a>
<br/>
## Support
If Reasonix has been useful and you'd like to say thanks, you can. It stays a coffee, not a contract — donations don't buy feature priority or change how issues get triaged.
fmt.Fprintf(os.Stderr,"error: repair plan preview changed since confirmation; re-preview and re-confirm (expected %s, got %s)\n",expectedPreviewID,actualPreviewID)
The user message is a diagnostic report whose findings carry only a severity, a machine code (e.g. config.invalid_toml, derived.invalid_json, update related codes), and a generic scope; pick actions from those codes.
Snapshot IDs are host-issued aliases ordered newest first; pendingUpdate=true means rollback_update is available.
Allowed actions only (return an empty actions array when no safe action applies):
- repair_config with scope global or project
- restore_snapshot with snapshotId
- rebuild_derived_state with target tabs, projects, window, zoom, or all
- rollback_update with no parameters
Never request shell commands, credential changes, session-content edits, arbitrary paths, plugin execution, or source-code changes. Prefer the smallest reversible plan supported by the diagnostics. Do not invent snapshot IDs.`
// providerSafeReport is the strict allowlist DTO sent to the AI provider.
// It deliberately carries no free-form diagnostic strings or state metadata:
// finding messages can quote config lines, URLs, commands, and permission
// rules, while snapshot and update metadata can be edited on disk. Only fixed
// diagnostic vocabulary, booleans, and host-issued snapshot aliases leave the
fmt.Fprintln(os.Stderr,"update rollback after failed install failed:",err)
iffailedInstallBlocksLaunch(result,err){
fmt.Fprintln(os.Stderr,"error: the failed installer may have left a mix of two releases and the rollback did not complete; refusing to start. Re-run reasonix-guard to retry the rollback, or reinstall Reasonix.")
return1
}
}elseiffailure!=nil&&result.RolledBack{
fmt.Fprintf(os.Stderr,"Reasonix Guard restored %s after the %s installer failed.\n",result.ToVersion,result.FromVersion)
fmt.Fprintln(os.Stderr,"error: the installation may mix two releases; refusing to start. Re-run reasonix-guard to retry recovery, or reinstall Reasonix.")
return1
}
forceSafeMode=true
}
}elseifresult.RolledBack{
fmt.Fprintf(os.Stderr,"Reasonix Guard restored %s before launch after an unfinished %s update.\n",result.FromVersion,result.ToVersion)
}elseifresult.Cleared{
fmt.Fprintf(os.Stderr,"Reasonix Guard cleared an abandoned %s update before launch.\n",result.ToVersion)
fmt.Fprintln(os.Stderr,"error: the installation now mixes two releases; refusing to start. Re-run reasonix-guard to retry the rollback, or reinstall Reasonix.")
return1
}
// The compensated install is still a coherent (new-version) release
// unit, so a Safe Mode boot is a sound fallback while the pending
// transaction stays on disk for the next rollback attempt.
useSafeMode=true
}elseifresult.RolledBack{
fmt.Fprintf(os.Stderr,"Reasonix Guard restored %s after %s failed to start.\n",result.ToVersion,result.FromVersion)
script:=`button returned of (display dialog "Reasonix failed to start repeatedly. You can repair damaged configuration or start with external integrations disabled." with title "Reasonix Recovery" buttons {"Quit", "Safe Mode", "Repair and Start"} default button "Repair and Start" with icon caution)`
"--ok-label=Repair and Start","--cancel-label=Safe Mode","--extra-button=Quit")
out,err:=cmd.Output()
ifstrings.TrimSpace(string(out))=="Quit"{
returnrecoveryQuit
}
iferr==nil{
returnrecoveryRepair
}
returnrecoverySafeMode
}
ifpath,err:=exec.LookPath("kdialog");err==nil{
err:=exec.Command(path,"--warningyesnocancel","Reasonix failed to start repeatedly. Repair configuration and start?","--title","Reasonix Recovery").Run()
Code and diff rendering go through two components with stable prop contracts and a
lazy boundary, so a heavy editor stays out of the initial bundle and dropping one
in is a one-line change — no consumer touches:
Code and diff rendering go through two components with stable prop contracts and
lazy boundaries, so heavier viewers stay out of the initial bundle. `CodeViewer`
keeps the compact highlighted viewer for chat, Markdown, and tool output, while
workspace file previews opt into the searchable line-number viewer:
| Component | Props | Default impl | Upgrade |
|---|---|---|---|
| `components/CodeViewer.tsx` | `EditorProps` | `editors/PlainCode.tsx` (`<pre>`) | swap the lazy import for `editors/MonacoCode` or `editors/CodeMirrorCode` |
| `components/DiffView.tsx` | `DiffProps` | `editors/PlainDiff.tsx` (LCS line diff) | swap for `editors/MonacoDiff` or `editors/CodeMirrorMerge` |
| `components/CodeViewer.tsx` | `EditorProps` | `editors/HljsCode.tsx`; `editors/LineNumberCode.tsx` when `showLineNumbers` is enabled | extend the implementation selection for Monaco or CodeMirror |
| `components/DiffView.tsx` | `DiffProps` | `editors/HljsDiff.tsx` (highlighted LCS/unified diff) | swap for `editors/MonacoDiff` or `editors/CodeMirrorMerge` |
t.Errorf("%s native icon could not be extracted",spec.View.Name)
}
}
}
Loaded 100 of 1597 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.