A session method refused because another runtime owns the transcript answered
-32600 and prose, the code a malformed frame gets; an ownership probe that could
not decide answered -32603, the same as a disk that would not write. A client
could not tell "held" from "unreadable" from "your request was broken", so it
could not decide whether to ask the person to close a window or retry.
session/new, session/load and session/resume now answer -32022 with data.code
busy.session_in_use for a held lease and -32023 with data.code
session.lease_unprovable when the store's probe cannot prove ownership either
way; -32600 is left to a genuinely malformed request. The human message stays.
The HTTP frontend, the kernel, the lease and the inbox are untouched.
docs/ACP.md states the table and what a client does with each answer. repolint's
family-vs-writer check is now parameterised by family; it reads the lease
sentinels beside the inbox ones, so a lease sentinel added without a coded case
fails by name against the ACP writer.
Every inbox handler answered a store refusal with -32600 and prose, so a client
could not tell "gone" from "wrong state" from "may already have run" — and one
that retried on any error re-sent work the kernel refuses precisely because the
turn may already have run. The HTTP frontend already names 14 conditions; ACP
now answers the same names, with -32020 for "the queue refused this call" and
-32021 for "the work may already have run", and leaves -32600 to a malformed
frame alone.
docs/ACP.md states the table and each refusal's class (terminal,
retry-after-repair, do not retry). repolint's family-vs-writer check now reads
both writers, so a sentinel added to the store cannot reach ACP unnamed.
session/new answered -32603 for a model with no API key or no configured
provider, although initialize advertises a setup auth method. The cause is now
a provider.ErrKeyMissing / ErrNoProvider identity and the frontend maps it to
the ACP schema's -32000.
$/cancel_request cancels only the named request and answers -32800;
agent message chunks carry an opaque messageId; an MCP server's form or
page goes through elicitation/create when the client advertised the mode.
The prompt result carried a top-level transcriptPath the ACP schema does not
allow, and status_update / mcpStatus_update went to every client, which
standard SDK clients answer with a method-not-found error each. The two
notifications now need clientCapabilities._meta["reasonix.io"].statusUpdates
and the path moves under _meta["reasonix.io"].
The final-readiness pause answered session/prompt with the non-standard
stopReason "error", so a client that reads only message text took unverified
work for finished. The turn now ends with end_turn, the typed outcome in _meta
and a host-written [paused] line at the end of the streamed text.
stopReason "error" is not in the ACP prompt-turn enumeration, and the cause
travelled only on a vendor notification, so a text-only client saw an
unexplained failure. The turn now ends with -32603, the redacted cause as the
message and data.code turn_failed.
Preserve ACP frame-order assertions while skipping only MCP status updates,
and use t.Context in the serve regression test.
Report servers requiring launch approval as pending rather than failed in
controller, ACP and /mcp snapshots. Keep pending rows disabled without a retry
action while retaining their recorded transport and sanitized error.
Document connecting and awaiting-approval notifications, style the pending
indicator in both themes, and derive frontend state coverage from Go constants.
Add controller, endpoint, ACP and row-rendering regression coverage.
An ACP client had no way to bring back a Goal the session still held.
session/set_mode goal always drafts a new Goal, so the next prompt replaced
the retained objective and reset its counters, while the controller's
ResumeGoal/PauseGoal (reached through /goal in the CLI and Studio composer)
were never driven by a frontend directly.
ACP gains two vendor methods, advertised in agentCapabilities._meta with
schemaVersion 1:
- _reasonix.io/session/goal/resume re-enters a stopped, blocked, paused
or cancelled Goal with its objective, counters, todos and delivery
checkpoint. It does not start a turn; the next session/prompt continues
it. A running, complete or absent Goal fails with -32010.
- _reasonix.io/session/goal/pause pauses a running Goal at its next
continuation boundary, or fails with -32011.
On success both clear the turn-outcome label (cancelled, failed) the
status showed in place of the Goal state, return the Goal in the status
shape, publish a status_update with event "goal", and emit
current_mode_update when the mode moves. docs/ACP.md documents that a
"failed" status labels the last turn while the Goal keeps running.
serve drives the same capabilities through POST /goal/resume and
POST /goal/pause, answering {changed, status, goal}, so frontend-parity
holds without a scope exception.
The rule that a running Goal cannot be resumed moves into
goalMachine.resume under its lock. Before, resume accepted a running Goal
and bumped its continuation epoch, dropping the continuation in flight;
this also affected /goal resume in the CLI and Studio.
initialize/authenticate move unchanged to handshake.go so service.go stays
inside its size budget.
Tests:
- acp: TestServeGoalResumeKeepsTheRetainedObjective and
TestServeGoalPauseStopsARunningGoal fail with only the declarations
present (capability not advertised; -32601 method not found) and pass
after. TestServeGoalFailedLabelKeepsTheGoalRunning fails when pause does
not clear the label (pause reports "failed") and passes after.
- control: TestControllerResumeGoalRefusesARunningGoal and the new
TestGoalAdvanceResultCannotCrossGoalLifecycle subtest fail on the base
("resume accepted a goal that is already running") and pass after.
- serve: TestServeGoalPauseAndResumeKeepTheObjective.
An ACP client that sent a picture got an answer from a model that never
saw it. session/prompt flattened its blocks to text and kept only text
blocks and resources with inline text: an embedded resource carrying a
base64 blob, which a client sends because embeddedContext is advertised,
was dropped without a word, and so was any image block. An image-only
prompt was refused as empty.
Image blocks and blob resources are now decoded and stored with the
control package's attachment savers under .reasonix/attachments/ in the
session's workspace, and the prompt carries the "@path" reference that
serve and the CLI already hand a turn. Undecodable data or bytes that do
not sniff as an image fail the prompt with invalid params instead of
vanishing. Steer uses the same path. initialize now advertises
promptCapabilities.image = true.
Test: prompt_attachments_test.go drives session/prompt through the real
Serve loop and a real controller. On origin/studio the image block and
blob resource cases fail with the turn input "what is in this picture?"
(no reference), the image-only case fails with "empty prompt", and the
undecodable case returns no error; all pass after the change, and
go test ./internal/frontend/acp/ passes.
Studio has no bot surface. On this line the QQ / Feishu / WeChat gateway
is reachable only as `reasonix bot`, a CLI command no Studio user starts,
and it carries about 12.9k lines: internal/bot, internal/botruntime, the
subcommand, its config section and its docs. It is also where the recent
Windows heap-corruption and gateway-wedge fixes had to land.
What goes with it:
- `reasonix bot`, its completion spec and its usage line in three locales.
- The typed [bot] config, its defaults, render block, model-ref and
migration hooks, and the 0.x QQ import.
- GatewayAPI, the control port only the gateway named, and the two
passive inbox variants only it called; neterr.IsTransient and
textutil.FitGraphemeBytes, which only it used.
- The bot entries in repolint's layer and frontend-parity declarations,
the bot race-test target in CI, BOT_GUIDE.md and its images, and the
[bot] example.
What stays, and why:
- The user config is shared with 1.x installs, which still run the
gateway. [bot] now decodes into an opaque table and is written back
unchanged on every save, so a settings change here does not switch
that gateway off or erase its allowlists. Credential variables the
table names are still kept out of subprocess environments.
- surface.Bot stays: usage records on disk carry it, and dropping it
would re-attribute them to another frontend.
- SECURITY.md and the crash-report worker still describe the bot, since
they serve every release line.
Budgets only fall: frontend-parity drops by 20 edges and the reclaimed
headroom is recorded with -update.
68b031ae5 was pushed with only its deletions and renames staged. Its
`git add` named CHANGELOG.md, which was already removed, and the failed
pathspec skipped every modification after it. That commit alone does
not build:
- productdocs still imports reasonix/release-notes;
- repolint still calls checkSpecParity;
- the extension golden test reads the old fixture path.
This commit carries the changes 68b031ae5 describes: productdocs and
docs-manifest without the release catalog, repolint without spec-parity,
the golden test on testdata, the /docs tests, the link fixes, ci.yml,
CODEOWNERS, the standard, the theme generator and the lowered baseline.
Verified locally on this exact tree before 68b031ae5 was pushed:
go test ./... passes and make lint is clean.
Resolve concurrent changes in desktop preset switching, frontend event contracts, bundle budgets, tests, and repository lint state.
Preserve the in-place execution-setting switch and completion-summary routing while retaining the latest catalog, billing, hydration, and runtime behavior from main-v2.
Verified with full Go and desktop tests, frontend test:all and production build, race checks, go vet, and repolint.
* feat(billing): unify multi-currency cost accounting
Problem:
Reasonix exposed provider prices, wallet balances, and session totals through independent currency paths. Mixed model runs could clear or mislabel costs, and display preferences could mutate regional list prices.
Root cause:
The runtime had no currency-tagged money domain, occurrence-time valuation, or shared quote boundary. Individual sinks accumulated floating-point Pricing.Cost values and inferred display semantics locally.
Fix:
Introduce fixed-point Money, CostQuote, official regional price catalogs, ECB FX snapshots, occurrence-time ledgers, wallet conversion, config v6 migration, and a central quote sink shared by Desktop, Serve, CLI, ACP, Remote, stats, and eventwire. Preserve additive legacy aliases and document the new contracts in English and Chinese.
Verification:
- go test ./...
- (cd desktop && go test ./...)
- (cd desktop/frontend && pnpm typecheck && pnpm test:usage-stats && pnpm build)
- go test -race ./internal/billing ./internal/event ./internal/stats ./internal/boot ./internal/config ./internal/cli ./internal/acp ./internal/serve
- go test -race ./internal/billing ./internal/acp ./internal/eventwire ./internal/event
- golangci-lint run --timeout=5m
- (cd desktop && golangci-lint run --timeout=5m)
- go run ./tools/repolint
- ./scripts/cache-guard.sh
- CACHE_IMPACT_PR_BODY_FILE=/tmp/reasonix-billing-pr-body.md ./scripts/check-cache-impact.sh
- DOCS_IMPACT_PR_BODY_FILE=/tmp/reasonix-billing-pr-body.md ./scripts/check-docs-impact.sh
- git diff --check
* fix(billing): keep cost totals complete and lossless
Problem:
Exact original-currency totals could be marked incomplete when an unused FX valuation was unavailable. CLI and ACP recomputed authoritative totals from a 64-quote sample, and live controllers retained the pre-refresh FX table pointer.
Root cause:
CostQuote completeness conflated cost-fact availability with optional valuation coverage. Run surfaces used bounded quote slices as ledgers, while QuoteContext captured a one-time RateTable snapshot.
Fix:
Make completeness follow the selected shared display total, preserve unrecoverable fact failures, and keep incomplete frontend quotes unavailable. Accumulate CLI and ACP totals through the billing ledger, including restored ACP scalar history, and read the current FXCache snapshot for every quote.
Verification:
- go test ./...
- go test -race ./internal/billing ./internal/event ./internal/boot ./internal/cli ./internal/acp
- (cd desktop && go test ./... && go vet ./...)
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend build
- ./scripts/cache-guard.sh
- go run ./tools/repolint
* refactor: remove runtime FX from billing
Problem: runtime FX downloads and wallet conversion mixed pricing estimates with provider balances and could fabricate cross-currency totals.
Root cause: CostQuote, ledger aggregation, display selection, and wallet rendering shared one currency path and treated display completeness as cost completeness.
Fix: keep fixed-point original facts, restrict new valuations to identity and official regional tables, add explicit cost/display status and currency buckets, remove ECB startup/cache paths, and use exact original wallet balances with runtime-only hints.
Verification: go test ./..., desktop go test ./..., targeted race suites, frontend typecheck/usage-stats/build, cache guard, cache-impact review, and secret scan.
* Fix display-only currency rebinding
Problem: changing the billing display currency still rebuilt Desktop controllers, stale wallet responses could bind a newer session, and two obsolete locale-pricing helpers failed golangci-lint.
Root cause: the display preference reused the general runtime-settings rebuild path, while wallet callbacks validated only the controller pointer and had no tab/session display generation.
Fix: persist the preference without rebuilding, re-select existing ledger valuations across runtime tabs, compare-and-apply wallet hints against controller and telemetry generation, and remove the obsolete locale-pricing paths.
Verification: focused RED/GREEN tests; Desktop focused race tests; go test ./... in root and desktop; go vet; golangci-lint; repolint; cache-impact check.
* Clarify cost display currency settings
Problem: the Desktop setting still used ambiguous pricing terminology and packed the auto-selection rule into the segmented control.
Root cause: the billing refactor renamed the preference but left the surrounding appearance copy and compact option label partially aligned with the old pricing semantics.
Fix: present one cost-display preference, mark auto as recommended, explain wallet, pricing-table, fallback, and mixed-currency behavior in the hint, and align English, Simplified Chinese, and Traditional Chinese copy.
Verification: source contract scan; pnpm typecheck; pnpm build with bundle budgets.
* Shorten cost display currency guidance
Problem: the Desktop cost-display hint wrapped to roughly four lines and visually dominated the setting row.
Root cause: fallback and mixed-currency details duplicated behavior already surfaced by the cost UI.
Fix: retain only the display-only contract and automatic currency-selection order in concise English, Simplified Chinese, and Traditional Chinese copy.
Verification: in-app browser DOM measurement at 904x936; pnpm typecheck; pnpm build with bundle budgets.
* Fix narrow settings control overflow
Problem:
- General settings kept its two-column layout at narrow desktop widths, clipping controls at the modal edge.
Root cause:
- A component-specific 900px override won over the shared 980px responsive rule and sized the row independently of its actual content pane.
Fix:
- Make the general page an inline-size container and stack fields below 620px of available width.
- Align the media-query fallback with the shared 980px breakpoint.
- Add responsive and startup contract coverage.
Verification:
- pnpm test:settings-responsive
- pnpm exec tsx src/__tests__/startup-settings-contract.test.ts
- pnpm check:css
- pnpm typecheck
- pnpm build
- 904x936 browser DOM check: maximum control overflow is 0px
Problem: User-facing role terminology suggests a persona switch, while these presets actually control planning, verification, and delivery behavior.
Root cause: Desktop, CLI, ACP, and documentation retained the earlier role-setting product name.
Fix: Use Execution setting / 执行设定 / 執行設定 consistently across product surfaces while preserving agent_preset, work_mode, tokenMode, and compatibility values.
Verification: go test ./internal/i18n ./internal/cli ./internal/acp; cd desktop && go test ./...; cd desktop/frontend && pnpm test:all && pnpm build; local browser menu click and overflow check.
The paragraph said Reasonix routes "eligible file operations" through the
editor's buffers, which was true only of read_file and write_file. Now that
every file tool routes through the overlay, say so, and name the one case
that stays local: a non-UTF-8 file, since the ACP file methods are text-only.
Stage 10 of the unified extension kernel / Extension Protocol v1 plan.
- New docs: EXTENSIONS.md/.zh-CN.md (capability map, install/manage,
fail-atomic reload semantics, development guide, compatibility,
full-trust security model) and EXTENSION_PROTOCOL.md/.zh-CN.md (prose
companion to the generated index: transport, lifecycle, content refs,
interception, streaming providers, structured UI, errors, v1 stability
contract, security model).
- Updated docs: PLUGIN_PACKAGES (Manifest v1 chapter: strict parsing,
merge rules, prompts/themes, runtime declaration, FULL TRUST disclosure
and --link trust inheritance), CAPABILITY_DIAGNOSTICS (additive v1
prompts/themes/runtime fields, schema_version stays 1), CLI (/reload),
ACP (_reasonix.io/session/reloadExtensions, extensionSurface
negotiation, /<plugin>:<action> discovery), RELEASING (sdk/go/vX.Y.Z
tag convention, three-product tag contract unchanged), THEME_PACK
(plugin theme kind and fallback-preserve, from stage 4).
- CI: new sdk job on the three-OS matrix (gofmt, vet, stdlib-only guard,
test, race on pushes) triggered by sdk/ or internal/extension/ changes;
root conformance tests run in the existing root matrix.
- Makefile: sdk-test / sdk-test-race targets.
- Bundle budget: initial raw JS+CSS 2250 -> 2265 KiB with a comment
recording the extension UI surface as the cause; every gzip budget
(the user-visible transfer cost) is unchanged and passing.
Compatibility audit (kept green by tests added in stages 1-9): legacy
manifests parse byte-identically, plugin-packages.json schema unchanged,
session JSONL untouched with extension state isolated to
<session>.extensions.json, desktop theme state preserves plugin: IDs
across uninstall while legacy files load unchanged, install-plan JSON is
omitempty-additive, plugin/... refs degrade to model-unavailable on old
versions, and the two new event kinds append at the enum end with
text fallbacks for old clients.
The Session controls table names the method and the option ids, but never the
request shape, so a client has to guess the field name. The natural guess is
optionId (it is setting an option, and the ids come from configOptions), and
that fails in a way that points at the wrong thing: the handler answers
-32602 session/set_config_option: unknown config option
with an empty name interpolated at the end, which reads as "that option does not
exist" rather than "you named the parameter wrong". The option is in fact
advertised correctly in session/new's configOptions.
Cost when a client gets this wrong is not cosmetic: tool_approval silently stays
at 'ask', so an unattended host blocks on session/request_permission for every
gated call instead of running.
Documents the params in both language versions, with a worked example, an
explicit note that the field is configId, and the result/error shapes.
* 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(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]>