Commit Graph
15 Commits
Author SHA1 Message Date
SSU-WEI HUANGandGitHub 0f7a3da68b feat(cursor): mid-turn Steer via concurrent ACP session/prompt (#888) (#1609)
* feat(shared): steer capability gates and live steered signal schemas

- STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession gate which
  agents can deliver queued messages into the active turn (pi, codex,
  cursor ACP; legacy stream-json cursor excluded)
- AgentState.steeringActive, DecryptedMessage.steered and
  messages-consumed  live signal (never persisted by the hub)

* feat(cli): queue reservations and steered messages-consumed option

- MessageQueue2 gains takeByLocalId/restoreReservation/
  beginReservationDispatch/commitReservation so an async steer can reserve
  a queued row without racing the main loop's turn/start drain
- emitMessagesConsumed accepts steered: true to mark mid-turn delivery

* feat(codex): mid-turn steer via app-server turn/steer (#888)

- CodexAppServerClient.steerTurn + TurnSteerParams/Response types
- CodexRemoteLauncher registers the steer-queued-message RPC handler:
  reserves the queued row, validates it against the active turn (no
  control commands, matching mode hash), injects via turn/steer with an
  epoch guard that invalidates in-flight steers on abort/cleanup
- steeringActive agent state tracks the active-turn window
- hub syncEngine gate opens to codex; messages-consumed relays steered

* feat(web): Steered badge and steer gating for codex sessions

- HappyUserMessage shows a ↳ Steered badge fed by the live
  messages-consumed steered signal, preserved across server echoes and
  refetches (mergeMessages carries the optimistic marker)
- SessionChat gates canSteer via isSteeringSupportedForSession instead of
  the pi-only check
- clearStaleQueuedStatus normalizes a queued status on an invoked message
- fix(web): drop duplicate showSessionSummaryInChat in markdown test
  (upstream typecheck breakage)

* feat(acp): split request dispatch from completion and add soft steer

- AcpStdioTransport.sendRequestWithDispatch separates stdin-accepted
  dispatch from the JSON-RPC response, keeping sendRequest behavior
  unchanged
- AcpSdkBackend tracks concurrent session/prompt requests with an
  activePromptRequests counter (main prompt + soft steers); response
  completion stays pending until every concurrent prompt settles
- beginSoftSteerPrompt kicks off a concurrent session/prompt (Cursor GUI
  Send semantics — no cancel, no handler swap) returning {dispatched,
  completed}; softSteerPrompt awaits the full response for direct callers

* feat(cursor): mid-turn soft steer via concurrent session/prompt (#888)

- CursorAcpRemoteLauncher registers the steer-queued-message RPC handler:
  reserves the queued row, rejects control commands and mode mismatches,
  then soft-injects via beginSoftSteerPrompt without canceling the
  in-flight turn
- Acks the hub once stdin accepts the inject (not on turn completion) to
  stay inside the 30s RPC window; the launcher stays busy until the
  concurrent prompt settles so handlers are not swapped mid-inject
- steeringActive agent state mirrors the active-turn window; abort and
  cleanup reset it and invalidate pending steers
- Legacy stream-json Cursor sessions register a steer handler that
  reports unsupported

* fix(codex,shared): address bot findings on steer gate and ambiguous turn/steer

- STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession advertise
  codex and pi only; cursor joins when its soft-steer handler lands (#1609)
- turn/steer now splits dispatch (stdin accepted) from completion (turn
  finished): the hub RPC acks once dispatch succeeds — never on the
  concurrent turn's completion, which can exceed the 30s RPC window
- queue row commits only after the turn settles; a rejected/aborted steer
  restores the row so the message still delivers via turn/start, and a
  dispatched steer is never restored (no duplicate delivery)
- steer carries clientUserMessageId (echoed as userMessage.clientId) so
  ambiguous transport failures can reconcile the thread later
- client tests cover dispatch/complete split and stdin-write failure

* feat(shared): advertise cursor in the steer gate now that its handler lands

Cursor ACP sessions pass the web and hub steer gates; legacy stream-json
cursor sessions stay excluded.

* fix(codex): reconcile dispatched steers before restoring; align error copy

- A dispatched turn/steer whose completion fails (disconnect / protocol
  error) is now reconciled via thread/read by clientUserMessageId before
  the queued row is restored — the instruction is only re-delivered by
  turn/start when the thread never received it
- Reconcile targets the pinned steer thread, not whichever turn is
  current when completion fails
- syncEngine unsupported-flavor error now matches the capability gate
  (Pi and Codex only until the cursor handler lands)
- launcher tests cover steer success (ack on dispatch), reconcile-accepted
  and reconcile-rejected outcomes

* fix(codex): consume the row at dispatch; drop background reconcile

- The hub RPC acks and the queue row is consumed as soon as stdin accepts
  turn/steer; completion is background-only logging. A dispatched steer is
  never restored, so the same localId cannot be re-delivered via turn/start
  after the caller was told the steer succeeded
- Dispatch failure (stdin write error) still restores the row and reports
  failure
- steer.completed rejection is always handled (no unhandled rejection on
  the dispatch-failure path)
- tests updated: completion failure after dispatch keeps the row consumed;
  dispatch failure restores it

* fix(cursor): consume the row at dispatch; keep waiters for prompt gating

- The hub RPC acks and the queue row is consumed as soon as stdin accepts
  the concurrent session/prompt; completion is background-only. A
  dispatched steer is never restored (no duplicate via the next prompt)
- softSteerWaiters are registered before awaiting dispatch so the main
  loop's finally cannot start the next prompt mid-inject; they still gate
  prompt handover on completion
- tests updated: post-dispatch ACP rejection keeps the row consumed

* fix(cursor,hub): never hang teardown on unresolved soft steer; align diagnostics

- Prompt-finally waits for soft-steer completion only when not exiting;
  the outer finally no longer waits at all — cleanup() disconnects the
  ACP transport, which rejects pending requests and settles the waiters
- syncEngine gate diagnostics and JSDoc name all supported flavors
  (Pi, Codex, Cursor ACP)
- regression test: Switch with an unresolved soft-steer completion still
  reaches teardown

* fix(codex): distinguish definite rejection from indeterminate completion

- Transport-level failures (timeout, abort, disconnect, spawn, protocol)
  carry an indeterminate marker; explicit JSON-RPC error responses do not
- After a dispatched steer, turn completion resolves → commit + consumed;
  a definite app-server rejection restores the row (instruction was never
  accepted, so turn/start cannot duplicate it); an indeterminate outcome
  leaves the row reserved so it can never be delivered twice
- Completion handling registers before awaiting dispatch so the
  dispatch-failure path cannot leak an unhandled rejection
- client/launcher tests cover explicit rejection (restore), indeterminate
  outcome (row stays reserved) and dispatch failure

* fix(codex): reconcile indeterminate steers instead of a permanent reservation

- After an indeterminate completion (disconnect/protocol), reconcile the
  thread by clientUserMessageId immediately: accepted → commit + consumed,
  provably rejected → restore, still unreadable → keep the reservation and
  retry from the main-loop top on later passes (post-reconnect)
- A row never sits in dispatching forever: the hub cannot stamp it invoked
  while the instruction may never have been accepted
- tests: indeterminate keeps reserved while thread unreadable; accepted
  reconciliation consumes; rejected path restores

* fix(cursor): abort drops soft-steer waiters so the next prompt never blocks

- Ordinary Abort (shouldExit false) now clears softSteerWaiters: the
  prompt finally cannot wait forever on a soft steer whose completion is
  unbounded; the ACP cancel rejects in-flight requests, and cleanup()
  settles leftovers on session end
- regression test: unresolved soft-steer completion after Abort no longer
  blocks the next prompt

* fix(codex): accept all thread item shapes; retry reconcile; ack through abort

- Reconcile matcher accepts userMessage/user_message with clientId/
  client_id, matching the shapes the thread parser supports — an accepted
  steer can no longer be misclassified as rejected
- A pending reconciliation schedules a wakeLoop retry, so a temporary
  app-server outage cannot strand the reservation behind waitForTurnOrRecovery
- The success-path ACK no longer checks the steer epoch: the hub already
  reported steered on dispatch, so commit + messages-consumed must reach
  it even when an abort resets the queue in between

* fix(cursor,acp): abort force-settles soft-steer bookkeeping

- AcpSdkBackend.abortSoftSteers() drops the concurrent-prompt counter and
  notifies response-complete so the next turn's waitForResponseComplete()
  cannot block on a soft steer that will never settle after abort
- handleAbort calls it before clearing the waiters; the main prompt's own
  finishPromptRequest stays guarded by Math.max(0, ...)
- unit tests cover counter release and no-op when idle

* fix(codex): reinit reconnected app-server; keep reconcile retries alive

- thread/read after a disconnect auto-connects a fresh app-server, which
  must be initialized before any request — reconcile now ensures
  connect + initialize (isConnected getter added to the client)
- every still-unknown loop-top reconciliation schedules the next retry,
  so recovery without external traffic is eventually observed
- launcher mock gains isConnected

* test(acp): match finishPromptRequest epoch signature in whitebox test

* fix(codex): timer-driven reconciliation; init tracking; abort-safe ACK

- Reconciliation runs on a self-rescheduling 1s timer independent of the
  main loop (wakes it too), so idle loops and waitForTurnOrRecovery still
  observe app-server recovery; abort clears nothing implicitly — the ACK
  path commits and consumes even when the reservation was cancelled
- Absence of a durable client id is ambiguous: unmatched reads stay
  'unknown' and keep retrying instead of restoring the row
- CodexAppServerClient tracks initialized state (reset on disconnect/exit)
  so ensureAppServerInitialized re-initializes a fresh process before
  thread/read; initialize failures leave the flag false for the next retry
- tests: accepted reconciliation via scheduled timer, indeterminate
  keeps reserved, explicit rejection restores

* fix(codex): bind reconciliation to the launcher lifecycle

- runSteerReconciliation clears any armed retry timer on entry and never
  installs a second one, so loop-top and timer-driven passes cannot
  multiply
- shuttingDown is set when the main loop ends: timers are cleared and the
  pending map is dropped, so an unresolved steer can never respawn an
  app-server after cleanup (remote-to-local switch included)

* fix(cursor): abort releases an in-progress soft-steer wait

- The prompt-finally wait races Promise.allSettled against the abort
  signal: an Abort that clears the waiters now also releases a wait that
  already started, so the launcher always reaches the next queued prompt

* fix(codex): report steered only after app-server acceptance

- The handler now awaits steer.completed (the inject-acceptance response):
  an explicit JSON-RPC rejection surfaces as failed and restores the row
  for the normal turn/start path instead of a false steered
- Transport failure after dispatch reports 'Steer outcome is being
  reconciled' and keeps the row reserved while the timer-driven thread
  reconciliation runs
- dispatch-failure path also swallows the paired completion rejection

* fix(cursor,acp): commit on ACP acceptance; distinguish transport failures

- AcpStdioTransport marks transport-level failures (timeout, closed,
  stdin write) as indeterminate; explicit JSON-RPC error responses are not
- The steer handler commits + consumes on completion (ACP acceptance) and
  restores the row on an explicit rejection; an indeterminate transport
  failure keeps the row reserved so a delivered instruction is never
  re-sent, and the ACK reaches the hub even when abort reset the queue
- launcher/transport tests updated for the three outcomes

* fix(steer): tri-state cancel, clear-safe reservations, bounded acceptance wait

- MessageQueue2.cancelByLocalId returns 'in-flight' for a dispatching
  steer reservation: the hub neither deletes the row nor stamps invoked_at
  (new CancelMessageResponse 'busy' status; web restores the optimistic
  row); pushIsolateAndClear and reset/close share cancelReservations so
  /clear-style commands cannot have a rejected steer resurrect a discarded
  prompt
- turn/steer acceptance wait bounded at 25s (< hub 30s RPC timeout): a
  lost response is indeterminate and funnels into thread reconciliation
  instead of stranding the reservation
- tests updated for the tri-state cancel contract

* test(cursor): match tri-state cancel contract for dispatching steers

* fix(cursor): drop duplicate promptInFlight declaration after upstream merge

* fix(codex,web): busy-aware edit flow; bound reconciliation reads

- QueuedMessagesBar edit flow treats a 'busy' cancel as unsuccessful: it
  never prefills the composer when the row is inside an async steer, so a
  second client cannot send a duplicate
- reconcileSteerByClientId bounds thread/read with a 5s timeout so a
  connected-but-silent app-server cannot hold the reservation in-flight
  indefinitely

* fix(steer): inFlight-dominated cancel acks; bounded reconciliation

- hub cancel-queued-message acks check inFlight before removed: a stale
  duplicate socket reporting removed can no longer delete the durable row
  while another socket is dispatching the steer
- reconciliation entries expire after 60s and mark delivered: after the
  rejection window, a dispatched steer that the app-server never proved
  (client ids dropped on restart) is committed instead of polling
  thread/read forever
- pre-dispatch failures (abort before write included) never enter
  reconciliation — they restore the row and report failure

* fix(cursor,acp,web): indeterminate close marks, steer gating precision

- AcpStdioTransport.rejectAllPending marks close/protocol failures
  indeterminate, so an accepted-but-close-interrupted soft steer restores
  nothing (no duplicate delivery)
- the abort race in the soft-steer wait removes its listener in finally
  (no accumulation across repeated waits)
- SessionChat gates the Steer button on agentState.steeringActive for
  codex/cursor instead of the queued-grace thinking flag, so Steer is not
  exposed before the launcher can accept it
- codex pre-dispatch abort never enters reconciliation (merged from #1606)

* fix(steer): persist indeterminate outcomes without replay

* fix(cursor): hold ambiguous steers for explicit resolution

* fix(steer): make ambiguous delivery restart-safe

* fix(cursor): make ambiguous delivery restart-safe

* fix(steer): recover crash-held rows and preserve retry dedup

* fix(steer): ack retries and bound stdin dispatch

* fix(cursor): reject steers when prompt generation changes

* fix(steer): reconcile indeterminate dispatches and serialize retries

* fix(cursor): preserve soft-steer reservations across abort

* fix(codex): classify stdin callback failures as indeterminate

* fix(steer): recheck indeterminate cancels after ACK

* fix(steer): close retry and abort races

* fix(cursor): hold ambiguous dispatch failures

* fix(steer): serialize live retries and abort admission

* fix(steer): distinguish live dispatching from unknown

* fix(cursor): bound ACP dispatch acknowledgements

* fix(steer): keep ACK failures held and reconcile busy cancel

* fix(cursor): preserve state when dispatch ACK is uncertain

* fix(steer): distinguish held cancel from removal

* fix(store): combine schema v24 migrations

* fix(cursor): distinguish held cancel from removal

* fix(store): reserve schema v25 for steer delivery state

* fix(cursor): suppress late ACP updates after abort

* fix(steer): keep held cancel state and notify requeue

* fix(cursor): isolate late updates after abort

* fix(steer): release explicitly cancelled unknown reservations

* test(cursor): cover explicit held cancellation

* fix(codex): reject cancelled reservations before native steer

* fix(cursor): reject cancelled reservations before ACP steer

* fix(codex): make reservation restore atomic with state

* fix(cursor): make reservation restore atomic with state

* fix(codex): terminate abandoned transport writes

* fix(cursor): hard-stop abandoned writes and update native queue state

* fix(steer): own abandoned app-server lifecycle and consume races

* fix(cursor): isolate aborts and add native retry resolution

* fix(codex): confirm dispatch and recover abandoned turns

* test(codex): mock abandoned transport callback

* fix(native): reconcile retry responses

* fix(codex): clear visible turn state on transport loss

* fix(steer): claim retries and cover native delivery state

* fix(native): resync busy cancel outcomes

* fix(native): preserve indeterminate state on Android hydration

* fix(steer): make retry claims single-winner

* fix(cursor): hold restore failures for explicit resolution

* fix(steer): serialize concurrent retry claims

* fix(socket): tolerate missing steer-state ACK callbacks

* fix(native): serialize retry operations

* docs(web): document unknown steer delivery and retry controls

* fix(steer): handle retry failures and abort-before-connect

* fix(cursor): drain foreground prompt after soft-steer abort

* fix(steer): reinitialize after transport loss and finish iOS retry errors

* fix(steer): preserve indeterminate rows across reconnect gaps

* test(web): mock indeterminate queued recovery state

* fix(steer): recover consumed ACK tombstones

* fix(steer): expose consumed cancel tombstones

* fix(cursor): drain soft steers before handler replacement

* fix(cursor): preserve buffered output on abort
2026-08-20 08:53:41 +08:00
SSU-WEI HUANGandGitHub ca6a6f0939 fix(cursor): retry transient ACP connection errors (#1541)
* fix(cursor): retry transient ACP connection errors

* fix(cursor): keep retry classification conservative

* fix(cursor): avoid retrying completed tool effects

* fix(cursor): require terminal retry failures

* fix(cursor): track retry activity across extension events

* fix(cursor): honor permission abort before retry
2026-08-16 22:42:10 +08:00
24e0c76717 feat(cursor): bump hub thinking on ACP harness wake (#1487)
* feat(cursor): bump hub thinking on ACP harness wake

When Cursor resumes after idle (notify_on_output / mid-idle ACP activity
or a permission request), flip thinking via the existing session-alive
keepalive so the hub list matches reality. Fixes #1470.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): emit thinking true/false edges for ACP harness wake

Address Codex Major on #1487: activity listener now reports idle as
false, and the launcher only keepalives on actual thinking transitions
so streamed chunks do not spam session-alive.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): reattach activity thinking listener after session/new remap

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 10:03:42 +08:00
fe4d51043c fix(cli): remap bracketed Cursor wires onto bare/SKU catalogs (#1430)
* fix(cli): remap bracketed Cursor wires onto bare/SKU catalogs

#1271 only handled the grok-4.5 legacy alias family. Persisted wires like
gpt-5.3-codex[fast=false] still failed agent --model on resume against
today's bare ACP catalogs. Remap non-legacy wires to bare bases / CLI
SKUs (and nearest configOption wires), and prefer spawn-safe ids in
session state.

Fixes #1428

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): harden Cursor stale-model remap against silent wrong picks

Address Codex Major review on #1430: prefer spawn-safe ids even on exact
bracket cache hits, require explicit wire params when ranking configOption
candidates, and stop downgrading unavailable CLI SKU variants.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): preserve desired Cursor variant across spawn-safe remap

Do not overwrite session.model with the bare/SKU spawn id before ACP
apply, and keep spawn-safe requested ids in wireIdForCursorSessionState
so apply does not re-poison hub state with bracket wires.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): remap-retry Cursor session/new on stale model reject

Initialize and session/load already retried once after Cannot use this
model; session/new had the same failure mode with an empty shared cache.
Mirror the one-shot remap+respawn path and cover it with a launcher test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): resolve Cursor apply against ACP option values first

Spawn-safe remap prefers bare/SKU ids, but set_config_option may only
accept full bracket wires. Resolve against the model option list before
falling back to metadata so apply does not pick a rejected bare base.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): fail Cursor launch when remapped spawn cannot restore model

When --model was remapped for spawn, restoring the original desired
variant via ACP is mandatory. Soft-failing left sessions running on the
wrong model. Cover success and hard-fail paths in launcher tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 20:49:42 +01:00
b7f52f58ca feat(media): add audio and file display (#1405)
* feat(cli): cross-flavor inline image display via MCP and ACP

Share display_image prompt across MCP-bridge flavors (Cursor, Gemini,
Kimi, Codex, Claude, OpenCode), auto-approve the tool in
buildHapiMcpBridge, handle ACP image content blocks, and harden
generated-image registration with content sniffing.

Closes tiann/hapi#956

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): render generated-image cards reliably in chat

Keep object URLs stable across refetch, upscale tiny inline images,
fetch generated-image bytes with cache no-store (avoid empty 304 bodies),
and load hapiMcpUrl from per-session API in hapi-display-image tooling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(cli+web): display_video MCP for inline mp4/webm (#956)

Add display_video alongside display_image, video MIME sniffing with avif
guard, web GeneratedImageCard video player, and hapi-display-image auto-routing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(cli+web): cross-flavor display_video parity with images (#956)

Share display_video prompts across MCP-bridge flavors, auto-approve the
tool, register mp4/webm via path sniffing, render inline video in web on
the existing generated-image RPC path, and restore robust media card fetch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): ACP image ordering and inline media source provenance

Flush buffered assistant text before async generated_image emit from ACP
image blocks (PR #958 review Major). Add optional source metadata on
generated-image wire messages (ingress, flavor, toolCallId, toolName) for
MCP, ACP, and Codex tool-result paths. Seeds artifact-event follow-up #966.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli+web): address PR #958 review Majors on media order and stale blobs

Queue ACP session updates and await async image registration before later
events; clear GeneratedImageCard blob state when imageId changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): await ACP queue after late-drain before turn_complete

Straggler session/update during drainLateBuffers can queue async image
registration; re-await sessionUpdateQueue so generated_image is not emitted
after turn_complete.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(scripts): route AVIF ftyp brands to display_image in helper

Match server-side detectImageMimeType so .avif files are not sent to
display_video and rejected as unsupported video.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(#956): agent inline-media doctor and discovery fixes

- hapi doctor inline-media: probe bridges, print per-session inline commands
- Expose hapiMcpUrl on session list summaries (stops false "no MCP" scans)
- Helper script: match cursorSessionId prefixes; HAPI_SESSION_ID path-only mode
- ACP bridge prompt: shell fallback + HAPI session id vs agent id rule

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): allow immutable cache for generated media blobs

Drop cache: no-store on generated-image fetch so browser can reuse hub
immutable responses; on 304 re-read via force-cache (#927, PR review).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(#956): Cursor native MCP overlay; drop user-turn bridge prepend

Cursor ACP ignores session/new mcpServers. Write .cursor/mcp.json and
run agent mcp enable hapi instead. Remove HAPI_MCP_BRIDGE_PROMPT from
user turns on ACP remotes; enrich MCP tool descriptions for discovery.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): preserve non-HAPI mcp.json keys on Cursor overlay cleanup

Cleanup only removes or restores the hapi MCP entry instead of rewriting
the full pre-session snapshot, so concurrent edits to other servers survive.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): handle generated_image in Grok ACP launcher switch

Upstream Grok launcher exhaustiveness broke after AgentMessage gained
generated_image for cross-flavor inline media.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): rebase fallout for display_video + OpenCode skill lookup

Gate display_video in the STDIO bridge, restore OpenCode first-prompt
TITLE_INSTRUCTION (skill_lookup), and update tool-list test expectations.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): leave user-owned hapi MCP entry alone on overlay cleanup

Only undo mcpServers.hapi when it still matches the exact entry this
session installed; concurrent Cursor/user edits of that key survive.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): exact-match auto-approve for display_image and display_video

Move media tools off substring name/id hints onto the exact-name set so
forged lookalike tools are not approved in default permission mode.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): drop dead Cursor bridge prompt; put guidance in MCP descriptions

Cursor must not get a user-turn media prepend (prompt-taint). Remove unused
HAPI_MCP_BRIDGE_PROMPT_CURSOR and embed DISPLAY_*_PROMPT_CURSOR in the
display_image/display_video MCP tool descriptions instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): require user approval for display_image and display_video

Those tools read arbitrary local paths into chat; keep them on MCP
approval_mode prompt and out of default-mode auto-approve exact names.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: re-trigger Codex PR review after provider 503

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): ignore URI-only ACP image blocks that read local disk

Passive ACP agentMessageChunk handling must not load file:// or bare
paths; local media goes through prompt-gated display_image/display_video.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): allowlist MP4 ftyp brands for video sniffing

Reject HEIC/HEIF and other non-video ISO-BMFF containers instead of
treating every non-AVIF ftyp as video/mp4.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): keep Cursor ACP startup if MCP overlay fails

Wrap installCursorMcpOverlay so a malformed project .cursor/mcp.json
cannot abort the session; continue without inline media tools.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): fail doctor inline-media when checks fail

Exit non-zero whenever required checks fail, even if an active
hapiMcpUrl bridge is present.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): expect display_video when change_title is disabled

Native ACP title mode still exposes display_image and display_video;
update startHappyServer test after rebase onto 0.23.4.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): keep ACP title sync synchronous outside media queue

session_info_update title forwarding (#1028) must not wait on the
async message-handler queue used for inline media ordering.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): sniff media headers only; advertise OpenCode display_video

Read 16 bytes for detectMediaTool instead of the whole file, and include
hapi_display_video in OPENCODE_NATIVE_TOOL_INSTRUCTION for remote ACP.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): show Cursor generate_image inline in HAPI chat

cursor/generate_image only emitted a tool card; register filePath or
base64 imageData into generatedImages and emit generated_image so the
web chat card renders (issue #956 / swear01 report).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): ignore path-only Cursor generate_image reads

Path-only filePath registration bypassed permission-gated display_image /
display_video MCP tools. Keep base64 imageData only; local paths must go
through MCP approval.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): gate inline media base64 length before decode

Reject oversized ACP/Cursor base64 payloads by character count so the
CLI never allocates past the 25 MB generated-image cap.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): require EBML DocType webm for inline video sniff

Bare EBML magic matches Matroska/MKV too; only accept DocType webm.
Also restore annotated Playwright cursor in annotatedVideoUseOption.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(scripts): read 128-byte header for WebM DocType sniff

detectMediaTool only loaded 16 bytes, so EBML DocType webm was often
missing and valid WebM files fell through to display_image.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): compare generated-image source by value in reconcile

Wire normalization allocates a fresh source object each pass; reference
equality forced media-card recomputation on every reload/SSE refresh.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): inject Cursor MCP enable for overlay unit tests

installCursorMcpOverlay always spawned `agent mcp enable`; tests now pass
a noop so the suite never shells out to a real Cursor binary.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): shell-quote doctor inline-media helper command

Paths and session prefixes with spaces/metacharacters broke the copied
snippet; JSON.stringify each interpolated argument.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(cli): fix doctor inline-media quote path expectation

Repo root from scriptPath is three levels up (cli/), not the parent of cli.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli/web): per-session Cursor MCP overlay id and bound tiny-image scale

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): redact generate_image base64 from logs and fix doctor MCP ids

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): harden inline-media doctor for packaged installs and hub headers

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): lock Cursor mcp.json updates and bound ACP media filenames

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): preserve mcp.json mode and token-scoped overlay locks

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): publish Cursor MCP lock owners via link(2) and treat EPERM as alive

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): fail closed on stale MCP locks; keep concurrent mcp.json top-level keys

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): roll back Cursor MCP overlay when agent mcp enable fails

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): await ACP session queue in suppressUpdatesDuring tests

#958 queues handleUpdate for media registration; upstream compact tests
assumed sync delivery after restore.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): capture ACP handler at enqueue; keep display_video manual

Close two Major review findings on #958: suppress queue leak after
restore, and Claude --allowedTools auto-approving local-path video.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: write through symlinked mcp.json; lazy-load inline video

Preserve user Cursor MCP symlinks on atomic overlay writes, and require
explicit Load video before fetching large generated-video blobs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): use valid TerminalToolDisplayMode in media card test

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(scripts): require unique session prefix in display helper

Reject ambiguous prefix matches so images/videos cannot land in the
wrong HAPI chat when multiple agent session ids share a prefix.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): always cleanup Cursor MCP overlay on teardown

Run overlay cleanup in finally so cancelAll/disconnect failures cannot
leave a dead hapi-<sessionId> entry in .cursor/mcp.json.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): handle Copilot generated_image; refuse MCP symlinks

Unblock typecheck after Antigravity/Copilot merge, and fail closed when
.cursor/mcp.json or .cursor is a project-controlled symlink.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: abort restore deliveryMode; prune dead Cursor MCP overlays

Unblock web typecheck after steer merge, and recover orphaned hapi-*
mcp.json entries via HAPI_MCP_OVERLAY_PID ownership stamps.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): recover dead-PID Cursor MCP overlay locks

Token-matched unlock so a crash mid-lock no longer permanently disables
inline media; keep live-owner waits identity-safe.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): serialize Cursor MCP stale-lock recovery

Acquire an exclusive recovery lock before token-matched unlink so two
recoverers cannot remove a successor's live mcp.json lock.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): fail closed on stale Cursor MCP overlay locks

Withdraw racy auto-recovery: pathname check-then-unlink/rename can steal
a successor lock. Stale locks throw with an explicit rm hint instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): drop duplicate deliveryMode on abort restore

Merge left both steer and queue; keep queue so retries after abort
do not re-bind to a later Pi turn.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): bound inline media reads on open fd

Close TOCTOU between pathname size check and readFile for display_image /
display_video and registerGeneratedImageFromPath. Also preserve non-PID
env edits on Cursor MCP overlay cleanup.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): repair happyMcpStdioBridge test syntax after merge

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): write Cursor MCP overlay to ~/.cursor, not the project

Keep ephemeral hapi-<sessionId> bridges out of the checked-out tree so
agents cannot git-add a live loopback URL. Tests inject mcpConfigDir.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): point Cursor MCP diagnostics at ~/.cursor/mcp.json

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(media): add audio and file display

---------

Co-authored-by: HeavyGee <133152184+heavygee@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
2026-08-08 13:46:59 +08:00
fdd286138a fix(cli): remap stale Cursor ACP model wires on resume (grok-4.5[fast=…] → cursor-grok-4.5-*) (#1271)
* fix(cli): remap stale Cursor grok wires on ACP resume (#1270)

When hub sessions still store legacy grok-4.5[fast=…] wires, remap to live
cursor-grok-4.5-* catalog ids before spawn and retry once on model_not_found.
Keeps #1198 honest errors when remap cannot find a candidate.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): address HAPI bot review on grok wire remap (#1271)

Stop Available-models parsing at newline/Tip; remap legacy wires even when
stale id remains in mixed availableModels+cliModelSkus cache.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(shared): rank catalog SKUs by fast hint before effort score

When medium-fast is absent, grok-4.5[fast=true] must not lose to slow
medium just because default effort scoring double-counts medium.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): stderr remap fallback + queued model sync (#1271)

Retry model_not_found remaps on the original legacy wire when cache
pre-resolution picked a stale SKU; enqueue user turns from session model.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(shared): reject unavailable SKU variants without ACP wires

matchCliSkuToAcpWireId no longer nearest-matches same-base CLI SKUs
when no wire exists; legacy grok remap stays on remapStaleCursorModelId.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): suppress transient model rejection on remap retry

Defer surfacing Cannot use this model stderr until initialize/load
retry fails; success path no longer shows a false error in chat.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 14:25:51 +01:00
d8aa8736cd fix(cli): surface real Cursor ACP session/load errors (#1198)
* fix(cli): surface real Cursor ACP session/load errors

Stop mislabeling every ACP session/load failure as a legacy
stream-json protocol problem. Prefer Cursor's Cannot use this model
stderr (including Available models when present), attach drained
stderr on process close, and keep structured formatAcpLoadError logs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): accumulate ACP stderr across split chunks

Address Codex Major on #1198: child_process stderr data events are not
message boundaries. Concatenate raw chunks in a rolling window, extract
Cannot use this model from the window on close, and prefer that over a
partial onStderrError hint when classifying resume failures.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): pin ACP model-rejection stderr when catalog overflows

Once Cannot use this model appears, keep the buffer from that match
head so a long Available models list cannot roll the rejection out of
the rolling window (Codex follow-up on #1198).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): wait for model id before ACP model-rejection emit

Only emit Cannot use this model via onStderrError once a non-space
token follows the colon, so a split before the id cannot suppress the
completed rolling-window message (Codex Minor on #1198).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): block ACP writes between process exit and close

Keep the post-exit stdin write guard while deferring markClosed until
stdio close so stderr can still enrich the failure (Codex Minor on #1198).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 10:05:41 +08:00
SSU-WEI HUANGandGitHub 40c1789357 fix(acp): sync native agent session titles (#1028)
* fix(acp): sync native session titles

* fix(acp): keep title refresh off turn path

* fix: reconcile native ACP titles with skill lookup

* fix: preserve OpenCode image tool instruction
2026-07-24 10:59:47 +08:00
af962fc61f fix(cli): stop prepending skill_lookup $name instruction onto user turns (#1096)
Cursor ACP (and other remotes) flagged the glued-on SKILL_LOOKUP_INSTRUCTION
as prompt injection. Keep discovery on the skill_lookup MCP tool description
and on system prompts (OpenCode/Grok); do not taint user messages.

Fixes #1095

Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 18:29:29 +01:00
SSU-WEI HUANGandGitHub f457156bd1 feat(cli): add skill_lookup MCP for non-native agents (#1035)
* test: reproduce issue #752

* fix: expose skill lookup MCP tool (closes #752)

* test: cover ACP skill lookup instructions

* fix: inject ACP skill lookup instruction

* test: narrow skill lookup auto-approval

* fix: restrict skill lookup auto-approval

* test: cover exact skill lookup tool names
2026-07-16 12:31:36 +08:00
2ab3b39887 fix(hub,cli): four hub-restart-cascade cleanup bugs (#913 #914 #916 #919) (#923)
* fix(hub,cli): four hub-restart-cascade cleanup bugs (#913 #914 #916 #919)

These four contained bugs were uncovered by a 2026-06-15 hub-restart
incident where `hapi-restart-hub` SIGTERMed 23 cursor ACP sessions.
Each fix lands independently of the architectural #915 (hub-restart
cascade-archive) and the hypothesis-pending #917 (reopen creates dead
session); audit-trail correctness and idempotency wins stand on their
own.

  Fresh ACP sessions could be SIGTERMed during the async `update-metadata`
  ACK round-trip, stranding the on-disk ACP store with no DB handle. Add
  `ApiSessionClient.flushMetadata()` and await it after `onSessionFoundWithProtocol`
  on the fresh-session branch. Resume-path pre-registration (PR #834) is
  unchanged.

  Hub-restart-cascade SIGTERMs went through the same path as web-UI
  Archive clicks, both writing archiveReason='User terminated'. New
  default is 'Hub restart'; the KillSession RPC handler (the
  authoritative user-archive signal) now explicitly stamps
  'User terminated' before cleanupAndExit. SIGINT (local-terminal Ctrl-C)
  keeps the 'User terminated' label too.

  `rpcGateway.killSession` threw a generic Error when no target socket
  was registered, and the archive route surfaced that as 500. Add typed
  `RpcTargetMissingError`, narrow on it in `syncEngine.archiveSession`,
  fall back to a hub-side `markSessionArchivedFromHub` write so
  lifecycleState still flips to 'archived'. Drop the requireActive
  guard on the route and 2xx-noop for already-archived rows.

  without refresh, producing forever-409 on rename/reopen until an
  unrelated event triggered a cache refresh. `renameSession`,
  `clearSessionArchiveMetadata`, `restoreSessionArchiveMetadata` now
  retry-with-refresh (5 attempts, then throw) mirroring the existing
  good pattern in `mergeSessions`.

Refs tiann/hapi#913
Refs tiann/hapi#914
Refs tiann/hapi#916
Refs tiann/hapi#919

AI disclosure: implementation by Claude Sonnet 4.5 (Cursor agent peer)
under operator supervision. Issue triage by a sibling discovery agent.
Per CONTRIBUTING.md AI-assisted contributions policy.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): runner-spawned children use 'Stopped by runner' as default archive reason

Addresses bot review of #923: with the #914 default-archiveReason flip to
'Hub restart', runner-driven SIGTERM paths (`hapi runner stop-session`,
webhook-timeout cleanup at run.ts:587, orphan-cleanup at run.ts:267) all
mislabel as 'Hub restart' which is also inaccurate audit-trail noise.

Smallest defensible change: parameterise the lifecycle default via
HAPI_DEFAULT_ARCHIVE_REASON env, and have the runner set
'Stopped by runner' on spawn. Terminal-launched sessions (no runner
parent, no env var) still default to 'Hub restart' since hub-restart
cascade documented at #915 is the most plausible SIGTERM source for
those. Explicit overrides via setArchiveReason (KillSession RPC, SIGINT
Ctrl-C, markCrash uncaught exception) still win.

Two new unit tests cover the env-var default and the override
precedence.

Refs tiann/hapi#914.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): markSessionArchivedFromHub surfaces persistence failures as 5xx

Addresses second-round bot review of #923 (Major): `markSessionArchivedFromHub`
silently returned on DB write errors and on exhausted version-retry
attempts, which would let `/archive` claim 200 OK while the row stayed
unarchived. That regresses the #916 acceptance criterion that non-RPC
errors during archive must still propagate as 5xx.

Both fall-through paths now throw, matching the contract of the
sibling writers in this file (renameSession, mergeSessions). The
sessionModel test suite gains two cases that spy on
`store.sessions.updateSessionMetadata` to force `error` and
`version-mismatch` shapes and asserts the helper throws. The existing
route test at `hub/src/web/routes/sessions.test.ts:1015` already
covers the route-level 500 propagation for any error thrown out of
`archiveSession`, so no new route test is needed.

Imports `spyOn` from `bun:test` to match this test file's runtime
(the rest of the hub package uses bun:test, not vitest).

Refs tiann/hapi#916.

Co-authored-by: Cursor <cursoragent@cursor.com>

* revert(cli): drop HAPI_DEFAULT_ARCHIVE_REASON env override

Reverts `1c8972a3`. Bot review round 3 surfaced that the env-on-spawn
approach (the bot's own round-1 suggestion shape) mislabels
hub-restart-cascade SIGTERMs against runner-spawned children: systemd
killcgroup on `hapi-runner.service` stop sends SIGTERM to all
runner-children directly, and those would archive as 'Stopped by runner'
instead of 'Hub restart'.

The two suggestions are mutually incompatible without adding an IPC
channel (stdio: 'ipc' on spawn) so the runner can stamp
setArchiveReason via childProcess.send() before SIGTERMing. That is a
refactor, not a smallest-defensible change.

Going back to the simple shape: SIGTERM default is 'Hub restart' for
everyone, runner-internal stop paths share that label. The
audit-trail-correctness criterion from the #914 issue is met
(SIGTERM no longer falsely labels as 'User terminated'). Finer
attribution between cascade vs runner-stop is deferred as a follow-up.

Refs tiann/hapi#914.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): clean completions get 'Session completed', not 'Hub restart'

Addresses bot review round 4 of #923 (Major): every agent runner
(runClaude, runCodex, runCursor, runGemini, runKimi, runOpencode)
calls setSessionEndReason('completed') on the natural exit path
without touching archiveReason. With the SIGTERM default flipped to
'Hub restart', clean completions were now archived as restart
cascades.

Fix: setSessionEndReason flips archiveReason to 'Session completed'
when it transitions to 'completed' AND no caller has already overridden
the archive reason. This covers all six agent runners with a single
setter change (no per-runner edits).

Two new tests cover the natural-completion default and the override
precedence (explicit setArchiveReason still wins).

Refs tiann/hapi#914.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): restore inactive-session guard on /archive except split-brain

Addresses post-rebase bot review Major on #923: dropping requireActive
entirely let normal inactive non-archived rows (completed stubs, UI
Delete/Reopen targets) fall through to archiveSession, which could stamp
archivedBy=hub on sessions that were never active.

Restore the 409 for inactive rows unless metadata.lifecycleState is
still 'running' (hub-restart split-brain cleanup case from #916).
Two route tests cover the guard and the exception.

Refs tiann/hapi#916.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): merge runnerLifecycle tests after upstream rebase

Post-rebase fix: Session completed tests referenced makeFakeSession
which was renamed to createMockApiSessionWithMetadataCapture when
merging upstream hasExplicitSessionEndReason tests with #914 archive
reason coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): pass lifecycle object to KillSession handler in Pi runner

Upstream #862 (Pi agent) landed after this branch was cut. runPi.ts
still registered the legacy bare cleanupAndExit callback, so web
Archive for Pi sessions would persist archiveReason: Hub restart
instead of User terminated. One-line fix matching the other six
agent runners.

Refs tiann/hapi#914.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 17:37:32 +08:00
02a0aa6733 fix(cursor): surface agent errors with warning styling in web UI (#871)
* test: reproduce issue #864

Assert Cursor error paths emit agent error payloads and web UI renders
them as warning-styled events instead of neutral session messages.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): surface agent errors with warning styling in web UI (closes #864)

Route Cursor stderr, init, prompt, and legacy exit failures through
sendAgentMessage({ type: 'error' }) and teach the web chat layer to
render error events with a warning icon instead of neutral info text.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 10:17:57 +08:00
26d3c2eb34 fix(hub+cli): defer mergeSessions on cursor ACP reopen until session/load succeeds (closes #939) (#948)
* fix(hub+cli): defer mergeSessions on cursor ACP reopen until session/load succeeds

Emit session-ready from the CLI after ACP load/newSession completes; hub
resumeSession and cursor dedup wait for that signal before merging rows so a
failed session/load no longer deletes the archived session the operator can retry.

Refs #917. Closes #939.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): gate session-ready wait on cursor ACP protocol only

Legacy stream-json Cursor resumes use cursorLegacyRemoteLauncher, which does
not emit session-ready; limiting the defer-merge and dedup gates to ACP avoids
60s resume_failed timeouts on those sessions.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): block ACP dedup until session-ready, including on session-end

Inactive ACP spawns that never emitted session-ready could still trigger
deduplicateByAgentSessionId on session-end and delete the original row.
Require session-ready for all ACP dedup paths and skip end-of-session dedup
when load never succeeded.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): restore session-end dedup for non-ACP cursor duplicates

Only skip the session-end dedup retry for Cursor ACP rows that never emitted
session-ready. Codex/Claude/legacy Cursor duplicates still merge when the live
row ends.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 10:10:10 +08:00
fa363c2f6c fix(cursor): register cursorSessionId before ACP session/load (#837)
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>
2026-06-08 13:31:23 +08:00
3a8693f380 feat(cursor): migrate remote sessions to ACP with model/variant pickers (#799)
* 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>
2026-06-06 19:51:35 +08:00