* fix(opencode): use ACP-reported reasoning effort options
Expose thought_level options from OpenCode ACP to the web UI via RPC/API
instead of hardcoded presets, and validate effort values before setConfigOption.
Fixes#852
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(opencode): sync hub effort after coerced setConfigOption
When resolveThoughtLevelEffort falls back to a different supported value,
roll back session state after a successful ACP update so keepalive and the
web UI do not keep advertising the rejected effort.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Pre-write resume token into session metadata before awaiting session/load
so hub and web see cursorSessionId immediately after resume spawn.
Fixes#834
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): dedupe sidebar sessions by flavor resume id
Wire deduplicateSessionsByAgentId into SessionList and resolve cursor
threads via cursorSessionId so resume/archive no longer shows duplicate
inactive rows for the same ACP session.
Fixes#833
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): use SessionSummary.agentSessionId for sidebar dedup
SessionList only receives SessionSummary from the API; native ids like
cursorSessionId are already mapped into metadata.agentSessionId by
toSessionSummary. Drop resolveAgentSessionIdFromMetadata to fix typecheck.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): hide inactive empty session stubs in sidebar
Filter inactive rows with no agentSessionId and no title signal before
grouping sessions, and expose lifecycleState on SessionSummary for future
sidebar rules. Completes the #833 P0 follow-up alongside agent-id dedup.
Fixes#833
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): scope sidebar dedup key by flavor
Prevent cross-flavor collisions when flattened agentSessionId retains a
stale native id. Add regression test and relax claudeRemote CI timeout.
Fixes#833
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes incomplete cliModelSkus while agent acp holds the CLI lock (#831) and
replace single-pid ACP lock with cross-process refcount (#832). Web picker
merges machine/session catalogs and waits for SKU readiness before showing
variant labels.
Fixes#831Fixes#832
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(cli,web,hub): migrate Cursor remote sessions to ACP with model/effort pickers
Move stream-json remote launcher to legacy path and add ACP launcher with
set_config_option model/mode sync, optimistic keepalive on config changes, and
shared catalog caching. Web gets dual base/effort Cursor pickers for session and
new-session flows; hide composer status bar when Cursor sends no usage_update.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli,web,shared): Cursor model picker — ACP wires + CLI sku variants
Enrich the web/mobile picker with agent --list-models SKUs grouped under
ACP wire bases, fix session-open base highlight, and keep catalog discovery
safe while the ACP transport holds the CLI lock.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor-acp): apply ACP default model when web resets to Default
Web sends model: null for Default; push session/set_config_option with the
ACP default[] wire so Cursor backend matches hub state. Regression tests
for setModel(null) and applyModelConfig(null).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(acp): clear stale agent-acp lock when owning process is gone
Check lock pid with signal 0; remove orphaned lock dirs after SIGKILL or
crash so listCursorModels can run cold probes again. Regression tests for
guard and catalog discovery.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(cursor): use live pid for ACP lock handler tests
Stale-lock cleanup clears dead pids; handler tests must simulate an
active lock with the current process pid to avoid cold probes/timeouts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(acp): scope agent CLI lock guard to Cursor agent command only
Gemini/OpenCode/Kimi ACP sessions must not register agent-acp-active;
that blocked listCursorModels while unrelated backends were running.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub,web): reject Cursor model changes for local sessions
Hub returns 409 when controlledByUser is set, matching Codex. Web hides
model and variant pickers for local Cursor sessions so users do not hit
a dead RPC path. Document pre-push-review in AGENTS.md.
Verified: bun typecheck; bun run test (919 cli + 243 hub + 768 web + 46 shared).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): send stable ids for Cursor ask_question replies
Parse and submit question.id and option.id so ACP receives keys like
{ approach: ['a'] } instead of index/label. Verified: bun typecheck && bun run test.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
`PermissionHandler` stored its own `permissionMode` field and only updated
it inside `handleModeChange`, which is called when a new batch is pulled
from the queue. The `SetSessionConfig` RPC (web dropdown changes) updates
`runClaude.ts`'s `currentPermissionMode` and the session keepalive
metadata, but never reaches the handler — so switching to Yolo mid-turn
left `canCallTool` checking the stale mode and still prompting for
approval. Closes#735.
Drop the stored field and read live from `session.getPermissionMode()`,
mirroring how the OpenCode permission handler already works. Override
`Session.getPermissionMode()` in `claude/session.ts` to return the
Claude-narrow `PermissionMode`, sound because the matching
`setPermissionMode` setter only accepts that subset.
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
PR #756 added a mid-turn emit in captureUsageUpdate to surface live
context usage via the web status bar. Testing against OpenCode 1.15.11
on a real session shows OpenCode emits a single usage_update per turn,
within ~1ms of session/prompt resolving — never during streaming.
That makes the mid-turn path dead code for OpenCode (and for any other
ACP agent that follows the same pattern). It also persists a useless
inputTokens:0/outputTokens:0 token_count message that gets immediately
overwritten by the finalize emit, churning the session history.
Drop the mid-stream emit and the activeOnUpdate plumbing it required.
Keep the finalize fallback for agents that don't return a usage block
on session/prompt (slash-handled turns, errored turns). The persistent
"live" counter requires the agent to emit usage_update during streaming;
filed upstream against anomalyco/opencode.
Refs #750
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
* fix(acp): flush straggler chunks promptly after session/prompt returns
After session/prompt returns, HAPI drains buffered agentMessageChunk text
and marks the turn complete, but leaves the message handler alive. Models
with long streaming tails (DeepSeek, GPT-5.5) continue to push chunks
after that drain, causing text to accumulate in the buffer and only appear
when the next user prompt triggers the pre-prompt drain — showing up in
the wrong turn with broken markdown.
Start a 50ms interval timer after the post-prompt drain that keeps calling
drainBuffers() on the live handler for up to 6 seconds, so straggler
chunks are emitted within one poll tick instead of waiting for the next
prompt. The timer is cancelled when the next prompt starts (pre-prompt
drain replaces the handler) or on disconnect.
Fixes#609. Also applies to Gemini and Kimi which share the same
AcpSdkBackend code path.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(acp): gate next turn's handler swap on previous turn's late drain
Addresses the github-actions review on #730: the fire-and-forget late-flush
timer let `prompt()` resolve while stragglers were still possibly arriving,
so a rapid follow-up prompt could either drop those chunks (during the
old null-handler gap) or leak them into the new turn's onUpdate.
Pre-prompt phase now keeps the previous turn's handler alive across the
quiet wait (bounded by LATE_FLUSH_WINDOW_MS) and swaps in a single phase
immediately before sending the new session/prompt. The post-prompt late
flush timer is unchanged — it still emits idle-window stragglers promptly
without delaying the ready signal or `setModel` / `setConfigOption`.
Adds three regression tests: late-chunk flushing within the window,
pre-prompt straggler attribution to the previous turn's onUpdate, and
disconnect cancelling the timer. Removes now-unused
PRE_PROMPT_UPDATE_DRAIN_TIMEOUT_MS.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(acp): await late drain so ready never fires before stragglers emit
Follow-up to bot's repeated MAJOR on #730: even with the pre-prompt gate,
the fire-and-forget late-flush timer let `prompt()` resolve before slow
tails finished, so the launcher's `ready` signal (and any user follow-up
queued against it) raced with text still being emitted to the current
turn's onUpdate.
Replace the setInterval timer with a synchronous `drainLateBuffers()`
awaited in `prompt()`'s finally before turn_complete is sent. It polls
drainBuffers every LATE_FLUSH_INTERVAL_MS so the UI keeps streaming
smoothly during the wait, and exits early once the model has been quiet
for LATE_FLUSH_QUIET_PERIOD_MS (250 ms — adds negligible latency to fast
models like Claude whose tail is typically <100 ms) or the
LATE_FLUSH_WINDOW_MS upper bound (6 s) elapses.
Side effects:
- Restore PRE_PROMPT_UPDATE_DRAIN_TIMEOUT_MS (1200 ms): the pre-prompt
drain is now just a safety net since the post-prompt wait guarantees
the previous turn is quiet by the time the next prompt starts.
- Drop the `lateFlushTimer` field, `startLateFlushTimer`,
`stopLateFlushTimer`, and their disconnect/pre-prompt cleanup calls.
- Update the "emits straggler chunks" test to assert ordering before
turn_complete, and add a fast-path test confirming the drain exits
promptly when the model is quiet.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(acp): anchor late-flush quiet window to entry, not stale lastSessionUpdateAt
Bot's third MAJOR on #730: drainLateBuffers() compared elapsed time
against lastSessionUpdateAt, which can already be older than
LATE_FLUSH_QUIET_PERIOD_MS by the time the method starts — e.g. when the
model emits chunks early in the turn, pauses, then sends stopReason. In
that case the first loop iteration sees a stale "quiet" reading and
returns immediately, missing any straggler that arrives just after
session/prompt resolves; the chunk then sits in the buffer until the
next prompt's pre-prompt drain.
Anchor the quiet check to max(lastSessionUpdateAt, entry time) so we
always observe at least one quiet period from method entry regardless of
when the last chunk was. Adds a regression test that fires a chunk
early, awaits a 200ms pause, schedules a post-resolution straggler, and
asserts it lands before turn_complete.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* docs(acp): correct LATE_FLUSH_QUIET_PERIOD_MS comment after entry-anchor fix
The previous note claimed the 250ms quiet check "exits early for fast
models, adding negligible latency". That was true before commit 512d6a4
when the check compared against lastSessionUpdateAt; with the entry-time
anchor, drainLateBuffers always observes at least one full quiet period.
Document that this minimum wait is the price of catching post-resolution
stragglers from paused-mid-turn models.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* chore(web): upgrade @tanstack/react-router to ^1.170.8
Fixes QuotaExceededError in scroll restoration: upstream @tanstack/react-router
>=1.145.6 wraps sessionStorage.setItem with try-catch, preventing the crash when
scroll restoration cache exceeds quota.
Refs: #683, #716, #721
* fix(web): adapt scrollStorageGuard to @tanstack/router-core >=1.145.6 API
`scrollRestorationCache` was removed from the public exports; replace with
`storageKey` import and simplify `hardResetScrollRestorationPersistedState`
to a plain `removeItem`. Remove the now-stale in-memory cache sync path and
its associated tests. Upstream try-catch (>=1.145.6) covers crash prevention;
this guard continues to proactively prune sessionStorage.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* feat(opencode): support plan mode
* feat(opencode): support reasoning effort
* feat(opencode): surface context usage in web
Bridge OpenCode ACP usage updates into the existing token-count pipeline so the web status bar can show live context and cache information without a separate UI path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(opencode): restrict plan mode to remote, rollback reasoning effort on failure
- Block local OpenCode plan startup (tools not enforced in local path)
- Allow remote OpenCode plan only (ACP permission handler denies tools)
- Guard web /permission-mode endpoint for local OpenCode plan sessions
- Rollback session reasoning effort when OpenCode rejects set_config_option
- Wire rollback callback through opencodeLoop to runOpencode closure
- Add tests: local plan rejected, remote plan allowed, web guard, effort rollback
* fix(web): auto-retry OpenCode models query to populate model selector without refresh
- Retry early failures (RPC may still be registering on new sessions)
- Poll briefly until availableModels is non-empty
- Stop polling once model options are discovered
- Add tests for retry/poll/stop policy
* fix(opencode): cap model discovery polling
---------
Co-authored-by: Cursor <cursoragent@cursor.com>