mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-09 19:29:41 +00:00
de93d7b977aee112b58b726eafccdadc8b5c8797
42
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e5a8212f4a | feat(session): validate agents and browse workspace directories | ||
|
|
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 |
||
|
|
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 |
||
|
|
7c4b3ab17b |
fix(cursor): stop session-list spinner flicker from ACP state_update (#1503)
* fix(cursor): stop session-list spinner flicker from ACP state_update #1487 mapped Cursor ACP state_update running/idle onto hub thinking. Cursor chatters those states while HAPI is queue-idle, so keepAlive flipped thinking every ~1-2s and the session list spinner danced. Ignore state_update for thinking; only bump on real activity, clear via prompt finally/abort. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): clear thinking on ACP idle without bumping on running Refine #1502 hotfix: ignore state_update running/requires_action (Cursor chatter caused spinner flicker) but still clear on idle so mid-idle harness wakes do not stick thinking=true forever. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): stop ACP background updates from flickering thinking Ignore tool/content session updates for hub thinking (ACP allows them while idle). Drive thinking from state_update only, debounce running 750ms, and skip idle clears during an in-flight HAPI prompt so #1502 residual flicker dies. --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
1761b696f7 |
feat: add cache-aware token usage dashboard (#1338)
* feat: add cache-aware token usage dashboard Track normalized Claude, Codex, and ACP usage with incremental SQLite backfill. Exclude imported transcript history, rebuild usage after history rewrites, and expose an owner-only dashboard with cache-aware totals and breakdowns. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix: preserve usage model and local dates via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix: normalize cached usage and timezone buckets via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> --------- Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
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> |
||
|
|
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> |
||
|
|
e4cc3916c8 |
fix(cursor): CreatePlan Yes accepts and continues task (nested outcome + plan→execute) (#1097)
* fix(cursor): nest ACP extension outcome so plan approvals aren't cancelled
Cursor's ACP blocking extension methods (cursor/ask_question,
cursor/create_plan) expect the JSON-RPC result to nest the outcome under
an `outcome` key, e.g. `{ outcome: { outcome: "accepted" } }`. The
adapter returned it flat (`{ outcome: "accepted" }`), so Cursor read
`response.outcome.outcome` as undefined and fell back to a cancellation
— an approved plan was relayed to the agent as `User cancelled`, making
plan mode unusable over HAPI for Cursor sessions.
Wrap every ask_question / create_plan response in the nested envelope
and add regression tests asserting the exact wire shape for the
affirmative approval -> CreatePlan path (plus approved_for_session,
reject, and abort).
Fixes #79
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor): continue task after CreatePlan Yes (plan→execute)
Nested ACP accept alone is not a complete fix: Yes unblocked create_plan
but the prompt turn still ended with the plan "done" and no execution.
Mirror Claude ExitPlanMode: on accept, leave plan/ask for an executable
mode and queue a continue prompt so the original user task keeps going.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor): sync runCursor enqueue mode after CreatePlan accept
Codex Major on #1097: setPermissionMode alone left runCursor's
currentPermissionMode stale, so the next user message could re-enter
plan/ask after Yes. Notify onPermissionModeChanged from CursorSession
so the enqueue source of truth stays aligned with the session.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
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 |
||
|
|
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> |
||
|
|
520c3f511a |
fix: verify Cursor chat store before reopen (#1037)
* test: reproduce issue #841 * test: cover Cursor chat store discovery * fix: verify Cursor chat store before resume (closes #841) * test: preserve non-Cursor resume behavior * test: cover conservative Cursor resume gating * fix: gate Cursor reopen until store verification * test: cover legacy Cursor drawer fallback * fix: scan unique legacy Cursor store drawer * test: preserve raw Cursor workspace path hashing * fix: hash raw Cursor workspace path * test: pin Cursor probe owner and machine * fix: probe Cursor store on recorded owner * test: normalize Cursor probe owner home * fix: normalize Cursor probe owner home |
||
|
|
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 |
||
|
|
73584e925a |
feat(cursor): multitask slash, autoReview mode, native worktree/add-dir (#1014)
* feat(cursor): multitask slash, autoReview mode, native worktree/add-dir Close the highest-value Cursor Agent gaps for remote HAPI: expand ACP-safe slash pass-through (/multitask, worktree, add-dir, …), add autoReview permission mode (--auto-review spawn + mid-session slash), and route Cursor New Session worktrees through agent --worktree instead of HAPI sibling trees. Fixes #1013 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): accept --mode autoReview for hapi cursor Align --mode parsing with CURSOR_PERMISSION_MODES so documented `hapi cursor --mode autoReview` enables Smart Auto instead of silently falling back to default. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
5ade952218 |
fix(cursor): support ACP parameterized model picker (#969)
* test: reproduce issue #968 * fix: support Cursor parameterized model picker (closes #968) |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
434cd9021d |
fix(cursor-acp): surface ACP stdin write failures to web UI (#870)
* test: reproduce issue #863 Co-authored-by: Cursor <cursoragent@cursor.com> * fix: surface ACP stdin write failures to web UI (closes #863) Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
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> |
||
|
|
ad038bbf2e |
fix(cursor): merge SKU catalog under ACP lock and refcount agent guard (#835)
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 #831 Fixes #832 Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
6d2d0d4707 |
fix(cursor): trim #784 safety patch to marker-only on legacy stream-json path (closes #822) (#828)
* fix(cursor): drop timing heuristic from #784 intercept; scan raw payload (#801 follow-up) PR #801 shipped a two-strategy intercept for the synthetic AskQuestion skip response in legacy stream-json mode. Real-traffic data from a post-merge run shows the marker-match strategy never fires (the converter's `extractToolResult` discards the marker for tool shapes it does not recognize, returning `{}`) and the timing-signature defense-in-depth strategy fires only on false positives - notably the Anthropic Vertex Claude tool calls cursor-agent surfaces in legacy sessions, which all land as `name=unknown` with the `{}` extracted result and frequently complete under the 500 ms threshold. Measured on a single legacy-resumed session (`7b769423`): 1,136 `name=unknown` tool calls, 16 rewritten as `no_input_surface`, zero actual marker strings stored anywhere in the session. The 16 rewrites were legitimate fast tool calls (Anthropic Vertex `toolu_vrtx_*` IDs) mischaracterized as fabricated skip responses. Changes: - Remove the timing-signature heuristic and its supporting state (started-at map, elapsed-ms calculation, latency threshold, test-only state reset). - Move the marker scan from the post-`extractToolResult` output to the raw `tool_call` payload, so it can see the marker on stream-json shapes the converter does not specifically recognize. Function-shaped tools exclude `function.arguments` from the scan to avoid matching agent-controlled input. Other shapes scan the full payload (no agent-input field exists at the top level). - Refresh tests: drop timing-based positive cases, add a marker-in-raw- payload positive case for `name=unknown` shapes, and add a regression that legitimate fast `name=unknown` tool calls without the marker pass through with `status: completed`. - Document scope: this intercept now lives only on the legacy stream- json path, which only resumed pre-ACP sessions hit. New cursor remote sessions go through `cursorAcpBackend` and the `cursor/ask_question` ACP extension method (#799) - immune to this bug. The intercept drains with the legacy session population. Tracking: #784. Builds on #801, complements #799. * fix(cursor): exclude agent input from marker scan; surface top-level Anthropic tool names (Codex P2) Codex flagged a false-positive case on the fork-stage review of this branch (heavygee/hapi#35, P2): an Anthropic tool_use shape with a top-level `name` (e.g. `{id, name: 'TodoWrite', input: { ... }}`) gets labelled `name=unknown` by the converter and passes the AskQuestion gate. If the agent's `input` quotes the synthetic-skip marker - which happens whenever an agent edits or documents this very bug - the intercept would rewrite a perfectly fine TodoWrite as a fabricated skip. Two-part fix: 1. `extractToolName` now reads the top-level `name` field as a final fallback. A real `TodoWrite` / `Bash` / `str_replace_based_edit_tool` surfaces with its actual name and is rejected by the AskQuestion gate before the marker scan runs. The original AskQuestion fabrication case still surfaces as `unknown` (per #784 issue body the name is stripped in the fabricated payload) and remains detectable. 2. Defense in depth: introduce `AGENT_INPUT_KEYS = {input, args, arguments}` and exclude these from the non-function shape's marker scan. Even if a tool reaches this code path with `name=unknown` and the marker buried in its `input`, the intercept won't fire on agent- controlled text. Two new regression tests: - Anthropic tool_use shape `{id, name: 'TodoWrite', input: {todos: [ marker]}}` → passes through with `status: 'completed'`. - `name=unknown` shape with marker only inside `input` → passes through with `status: 'completed'`. All 20/20 tests pass; typecheck clean (cli + web + hub). |
||
|
|
edc3acc3e2 |
fix(cursor): requeue user message on transient agent exit (auth, rate limit) (#823)
* fix(cursor): requeue user message on transient agent exit (auth, rate limit)
cursorLegacyRemoteLauncher.runMainLoop popped a user message off the queue
before spawning `agent` and silently discarded it whenever `agent` exited
non-zero (auth expiry, rate limit, transient network). The wrapper logged
the failure at debug level only, never surfaced it to the web UI, and
emitted `ready` as if a normal turn had ended.
Capture stderr from the spawned process; classify exit-1 with a transient
signature (Authentication required, rate limit, ETIMEDOUT, ECONNRESET,
EAI_AGAIN) as recoverable; re-head the message via `queue.unshift`, surface
a friendly banner via `sendSessionEvent({type:'message',...})`, and backoff
~2s before the loop picks it up again. Cap at 5 consecutive transient
failures, after which the message is dropped with a clear "resolve and
resend" event so we never spin forever on a genuinely broken auth.
Non-transient non-zero exits also surface the stderr to the UI now (instead
of only the local ring buffer), so a real crash is visible to the operator.
Backoff is overridable via CURSOR_LEGACY_TRANSIENT_BACKOFF_MS for tests.
Tests cover: success path, transient auth requeue + banner, rate-limit
banner, non-transient crash surfaced without requeue, and the 5-failure
drop cap.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor): preserve slash-command isolation on requeue; wait for stderr flush
Two findings from the cold-review bot on the requeue path:
1. `enqueueCursorUserMessage` uses `pushIsolated` for pass-through slash
commands (e.g. `/compress`) so they never batch with sibling prompts.
The transient-requeue path used plain `unshift`, which dropped the
isolate bit and allowed the next collected batch to merge the slash
command with a sibling - changing command semantics. Add
`MessageQueue2.unshiftIsolated` and use it when the popped batch was
isolated or when `parseCursorSpecialCommand` recognises the message.
2. `runAgentProcess` resolved on `child.on('exit', ...)`. Node may emit
`exit` while the stderr pipe is still draining, so a fast "auth
required" error printed-and-exited could be classified as
non-transient with empty stderr and silently drop the user message -
the exact bug this PR was supposed to fix. Resolve on `close` instead,
which waits for stdio streams to flush.
Adds a unit test that requeues `/compress` after a transient auth failure
and asserts the second spawn still receives the slash command alone (not
batched with a sibling).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor): restrict transient retry to exit code 1 only
Upstream codex review #823 (Minor): the helper treated any non-zero exit
with matching stderr as transient, which could requeue a signal-killed
(SIGTERM 143, SIGKILL 137) or crashed (SIGABRT 134) process whose stderr
happens to contain a keyword like "rate limit". Documented contract is
exit-1-for-transient; tighten the classifier accordingly.
Adds regression test covering exit 143 + rate-limit stderr → no retry.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor): clean up transientBackoff abort listener on timer completion
Upstream codex review #823 (Minor): transientBackoff added an abort
listener with { once: true } but only removed it when the abort fired.
Because the launcher reuses one AbortController, repeated transient
retries accumulated stale listeners until the next abort.
Switch to a single completion path that clears the timer AND removes the
abort listener whichever side wins.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor): cap in-memory stderr capture at 8 KB
Upstream codex review #823 (Minor): runAgentProcess accumulated every
stderr chunk for the full child lifetime. A noisy `agent` failure could
grow CLI process memory without bound even though only the first 400
chars are ever displayed. Cap the retained copy at 8 KB; debug log of the
full stream is unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
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> |
||
|
|
dc0d21e05b |
fix(cursor): intercept fabricated Questions skipped AskQuestion result in headless mode (#784) (#801)
* fix(cursor): intercept fabricated 'Questions skipped' AskQuestion result in headless mode (#784) When cursor-agent runs under `--print --output-format stream-json` (HAPI's current Cursor remote launcher), the CLI returns a synthetic `Questions skipped by the user, continue with the information you already have` response for the `AskQuestion` tool in ~zero seconds with no error flag, because there is no IDE surface to render the question. The underlying model can interpret this as legitimate user consent and act on it. This patch intercepts the synthetic result in `cli/src/cursor/utils/cursorEventConverter.ts` and rewrites the `tool_call`/completed event to a structured `no_input_surface` failure (`status: 'failed'`, which downstream becomes `is_error: true`). Detection has two strategies: 1. String match - any `tool_call`/completed payload whose serialized form contains the synthetic-skip marker is rewritten. This is robust to wherever cursor-agent stuffs the marker inside the `tool_call` object. 2. Timing + name heuristic (defense in depth) - any completion that arrives within 500 ms of its 'started' event with a trivial result, for a tool call named `AskQuestion`, `askQuestion`, `ask_question`, or the converter's `unknown` fallback, is also rewritten. This catches the case where cursor-agent changes the synthetic-string text in a future release. The converter tracks per-call timestamps in a bounded `Map` (`<= 1024` entries, oldest evicted on overflow) and clears entries when the corresponding 'completed' event arrives. A small test-only reset hook isolates state between Vitest cases. This is a transitional safety patch. It auto-deletes when #781's ACP launcher replaces the stream-json launcher and `cursor/ask_question` becomes a proper bidirectional ACP method where fabrication is structurally impossible. Scope is intentionally tiny: only `cli/src/cursor/utils/cursorEventConverter.ts`, its colocated Vitest file, and a section in `docs/guide/cursor.md`. No changes to `cursorRemoteLauncher.ts`, ACP code, web normalizer, or permission UI. Refs: tiann/hapi#781 (long-term resolution via ACP migration) Closes: tiann/hapi#784 * fix(cursor): gate AskQuestion intercept on tool name (#784 PR #801 review) Address regression flagged by the HAPI auto-review bot on #801: `containsSyntheticSkipMarker` previously stringified the entire `tool_call` payload and matched the literal marker substring. Because this PR also adds that exact marker to `docs/guide/cursor.md` (to document the intercept), a Cursor `read_file` of that documentation page would surface the marker inside `readToolCall.result.content` and be rewritten as a `no_input_surface` failure, corrupting an unrelated, legitimate result. The intercept is now gated on the tool name resolving to an AskQuestion-shaped call (`AskQuestion`, `askQuestion`, `ask_question`, or the converter's `unknown` fallback for unnamed function-shaped tools). `read_file` / `write_file` tool calls - which have explicit `read_file` and `write_file` names from `extractToolName` - no longer fall under the intercept, regardless of what their payload contains. The marker check itself now walks values recursively (string / array / object), guarded by a `WeakSet` against cycles, instead of relying on `JSON.stringify`. Slightly tidier; behaviour is otherwise unchanged for the AskQuestion path. Regression tests added: - `read_file` result whose `content` contains the marker -> passes through with `status: 'completed'` and no `no_input_surface`. - `write_file` whose serialized `args` contain the marker -> same. - A non-AskQuestion function tool (`MyCustomTool`) whose result quotes the marker -> same. All 846 cli tests pass (17 in this file). `bun run typecheck` exits 0. * fix(cursor): scope synthetic-skip check to extracted result (#784 PR #801 review-2) Address second Major finding from the HAPI auto-review bot on #801: After the previous fix gated the intercept on the tool name, the marker check still recursed into the entire `tool_call` object - which includes `function.arguments`, the agent's own prompt text. A legitimate AskQuestion whose prompt quotes the synthetic-skip marker (e.g. an agent debugging this exact bug, or any prompt that pastes the marker verbatim) would have been rewritten as `no_input_surface` even when the operator actually answered. Changes: 1. `extractToolResult` now extracts the cursor-side response from function-shaped tool calls. Previously it returned `{}` for anything that wasn't `readToolCall` or `writeToolCall`. It now returns `function.result` when present, otherwise every field of `function` except `name` and `arguments`. This excludes the agent's input from what downstream sees as the tool result, and as a side effect surfaces the actual cursor response for function-shaped tools (which was previously lost - see the #784 incident note about HAPI storing `output: {}` for AskQuestion in the message DB). 2. `shouldRewriteAsNoInputSurface` now searches only the extracted `result`, not the whole `tool_call`. The bot's exact recommendation. 3. Test added: an AskQuestion whose `arguments` quote the marker but whose `result` is a real user answer, with elapsed time past the 500 ms threshold so the timing heuristic does not apply. Asserts the tool_result passes through with `status: 'completed'` and the operator's actual answer. All 847 cli tests pass (18 in `cursorEventConverter.test.ts`). `bun run typecheck` exits 0. The widened `extractToolResult` scope is necessary for the marker check to actually find the synthetic string (it lives inside `function.result` or a sibling field), and is the bot's explicit recommendation. It also removes the long-standing data-loss bug where AskQuestion responses were surfaced to the message DB as opaque `{}` - regardless of fabrication. |
||
|
|
30564601ed |
fix(cli,web): hide Windows spawn windows and show queued attachments (#765)
* fix(cli,web): hide Windows spawn windows and show queued attachments * fix(web): preserve attachment-only queued edit text |
||
|
|
449cf6af0a |
feat(cli): wire Cursor /summarize and /clear slash builtins (#747)
* feat(cursor): wire /summarize and /clear slash builtins for remote sessions Seed cursor builtins for web autocomplete, parse summarize/clear in cursorRemoteLauncher (pass-through to agent -p; reject /clear with args). Fixes tiann/hapi#738 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): isolate slash commands before message queue batching Parse summarize/clear at enqueue time (runCursor) with pushIsolateAndClear so waitForMessagesAndGetAsString never merges a slash with the next prompt. Adds queue policy tests for invalid /clear + following message. Addresses PR #747 review. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): preserve pending messages when isolating slash commands pushIsolateAndClear() wipes the entire queue, so a normal prompt queued before /summarize or /clear would be silently dropped. Add pushIsolated() - isolation without clearing - and route Cursor slash commands through it instead. Adds queue tests covering the preserve-then-isolate path. Addresses PR #747 review. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
c58e8cea9e |
fix(cursor): persist resume id early and return 409 for resume_unavailable (#745)
Remote cursor launcher now mirrors local launcher by writing cursorSessionId to hub metadata as soon as --resume is known, before the agent init event. POST /sessions/:id/resume maps resume_unavailable to 409 with clearer guidance. Fixes tiann/hapi#744 Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
1d03f186d6 | feat(cursor): support model selection (#684) | ||
|
|
b0a3397601 | refactor: share session config RPC handling | ||
|
|
b79f50f815 | Share RPC method constants | ||
|
|
197f327590 | feat: add hapi resume command (#647) | ||
|
|
8185f0287e | feat(web,hub): cancel queued messages (#568) | ||
|
|
4ec9537e4a |
feat(hub): add ServerChan task notifications (#515)
* fix(hub): 修复发送后状态显示延迟 * feat(hub): 接入Server酱任务通知 * fix(hub): 仅在会话结束时发送完成通知 * fix(hub): avoid reviving inactive queued sessions * fix(hub): address notification review feedback * fix(hub): expire queued thinking on hub clock * fix(hub): 修复任务通知 review 反馈 |
||
|
|
32755f9056 | feat(web): show queued status for messages pending inference (#492) | ||
|
|
2f61852a9e | refactor: extract local agent spawn helper and unify process tree cleanup (#410) | ||
|
|
d76b1a6ac0 | refactor: introduce model-agnostic agent interfaces (#323) | ||
|
|
caa76826f4 | fix(cli): preserve invoked cwd for local launcher (#299) | ||
|
|
02c8e12e80 | unify model selection | ||
|
|
c9be2894ac |
feat(cursor): add support for Cursor Agent CLI integration (#236)
* feat(cursor): add support for Cursor Agent CLI integration - Introduced new command `hapi cursor` to start Cursor Agent sessions. - Added functionality for resuming sessions and managing permission modes. - Updated documentation to include Cursor Agent usage and installation instructions. - Enhanced existing codebase to accommodate Cursor as a recognized agent flavor. - Implemented local and remote session handling for Cursor Agent. This update expands HAPI's capabilities by integrating support for the Cursor Agent, allowing users to leverage its features alongside existing agents. * Remove TODO.md file as it is no longer needed following the integration of Cursor Agent CLI support. This cleanup helps streamline project documentation and reflects the completion of the associated tasks. * feat(cursor): implement remote mode and fix --hapi-starting-mode - Consume --hapi-starting-mode in cursor command (do not forward to agent) - Implement cursorRemoteLauncher: spawn agent -p with stream-json, --trust - Add cursorEventConverter for NDJSON parsing (system/assistant/tool_call/result) - Multi-turn via --resume session_id - Update docs: cursor supports both local and remote modes Made-with: Cursor * fix: type error * fix(cursor): address PR review - model UI, sessionId metadata, duplicate flags - HappyComposer: use isClaudeFlavor for model mode (cursor has no model modes) - cursorLocalLauncher: call onSessionFound for resume so cursorSessionId in metadata - cursorCommand: do not forward parsed flags to cursorArgs (avoid duplicates) Made-with: Cursor |