Commit Graph
7 Commits
Author SHA1 Message Date
MaizandGitHub b49c5d1dcb fix(cli): flush buffered ACP assistant text after an idle gap (#1898)
* fix(cli): flush buffered ACP assistant text after an idle gap

ACP gives no "text segment finished" signal for a turn's last text segment and AcpMessageHandler only emits buffered text at explicit boundaries or through the prompt-lifecycle drains. When neither arrives - a text-only turn whose prompt() never returns - the answer is held until the next user prompt.

Emit buffered text once the stream has been silent for TEXT_IDLE_FLUSH_INTERVAL_MS (1000ms). The idle flush defers while a reasoning block is open (keeps Reasoning above the answer) and while the buffer may be an unfinished control envelope (keeps the internal-event filter complete). A later boundary emits only the remainder, so no character is emitted twice.

* fix(cli): stream idle ACP text as snapshots under one stream id

An id-less idle emission could render a reordered or duplicated answer when a dedupe agent rewrites its buffer with a prefix, and a pause inside an open Markdown fence split the rendered answer into separate blocks.

Stream idle emissions as full-text snapshots under a stable stream id and finalize with the same id at the boundary, mirroring flushReasoning; the partial-watermark computation is removed. The control-envelope guard no longer assumes type is the first JSON key.

* fix(cli): skip interim idle text snapshots in local launcher displays
2026-10-28 12:08:26 +08:00
86c88df93b fix(runner,hub): cross-platform orphan reap on archive / tracking loss (#1911)
* fix(runner,hub): reap detached driver CLI on archive and tracking loss

Archive always verifies via stopRunnerSession; runner stopSession scans
argv orphans and awaits webhook-timeout tree-kill so PPID=1 children
cannot accumulate after KillSession miss or map drop.

Fixes #1910

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

* fix(cli): keep --hapi-session-id reap-only; test orphan kill path

Claude/cursor swallow the stamp without bootstrap reuse, and
reapRunnerSpawnedOrphans is unit-tested as the stopSession map-miss path.

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

* fix(cli,hub): address #1911 CI and bot Majors

Use Vitest in orphanReap tests, add Pi session.on mock, refuse archive
when machine StopSession is unreachable, trust full tree-kill results,
and argv-sweep orphans after every mapped stop.

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

* fix(runner): gate late-webhook kill on timeout orphans; fail closed on scan error

Track webhook-timeout PIDs separately so recovered shared roots are not
killed, and treat ps-list failures as still_alive rather than empty.

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

* fix(runner): keep shared Codex wrapper alive when archiving one root

StopSession must detach a single shared root without tree-killing the
wrapper PID while sibling roots remain, including after KillSession has
already marked the runtime inactive.

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

* fix(runner): keep shared Codex siblings after restart tracking loss

Before persisted-PID or argv orphan kills, consult the durable runtime
registry for active sibling roots on the same wrapper PID so archiving
one root after a runner restart cannot tree-kill live shared sessions.

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

* fix(hub,cli): deliver hub-authored archive metadata to reconnecting CLIs

When KillSession misses and archive is written via markSessionArchivedFromHub,
emit Socket.IO update-session to the session room. On reconnect, ApiSessionClient
reconciles GET /cli/sessions/:id metadata so hub-archived still fires.

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

* fix(runner): argv-scan orphans even when shared siblings remain

Remove session-wide early returns that skipped orphan reaping whenever any
shared wrapper had active siblings. Keep only the PID-specific filter so
older untracked CLIs with the same HAPI id are still reaped.

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

* fix(runner): protect tracked shared wrappers when runtime registry is empty

Argv orphan sweeps must exclude in-memory shared wrapper PIDs that still
host sibling roots, not only durable registry siblings — otherwise a missing
runtime record lets the sweep tree-kill the wrapper while archiving one root.

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

* fix(runner): adopt untracked nonshared CLIs after runner restart

When a runner-spawned Claude webhook arrives with no TrackedSession (typical
after restart mid-bootstrap), durably adopt the PID instead of ignoring it so
StopSession can reap. Keep killing only nonshared timeout orphans; never kill
shared Codex wrappers on late webhooks.

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

* fix(runner): keep recovered shared wrappers for registry siblings

After restart adoption tracks only a newly reported Codex root, archiving
it must consult the durable registry before tree-killing the wrapper PID
so older active sibling roots survive.

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

* fix(hub): do not archive on KillSession ack when StopSession is unknown

KillSession returns before cleanupAndExit finishes. When the runner cannot
find the PID (unknown), wait for the session KillSession handler to drop
before archiving; refuse if the CLI socket remains reachable.

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

* fix(hub,cli): confirm KillSession pid via StopSession before archive

KillSession now returns the CLI pid. When session-id StopSession is
unknown, archive confirms that OS pid (without treating socket loss as
exit) and still refuses when confirmation stays unknown or still_alive.

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

* fix(runner,hub): tree-kill adopted CLIs and require PID start markers

Adopted post-restart runner sessions now use killProcessTreeByPid when no
ChildProcess handle exists. KillSession carries a process-start marker so
raw PID confirmation cannot tree-kill a reused OS pid.

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

* fix(runner,hub): verify adopted PID markers and hub-archive after stop

Adopted sessions compare the persisted start marker before tree-kill and
drop stale tracking on mismatch. After confirmed StopSession, hub-authors
archive metadata when the CLI did not finish its flush.

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

* fix(runner): refuse unconfirmed shared/adopted stops

Keep shared wrappers when siblings remain, but return unknown unless
runtimeControl already ended the root. Adopted PIDs without a start
marker no longer tree-kill by tracked PID alone.

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

* fix(runner): require inactive binding before sibling-kept archive

Siblings alone must not flip StopSession to stopped on retry, while an
acknowledged inactive Codex registry binding remains stop proof. Handle
PID- markers before any tracked kill so reuse cannot nuke strangers.

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

* fix(runner): distinguish orphan reap from unknown keep-wrapper base

Pass unknown into finishWithOrphanSweep so a successful argv reap stays
stopped instead of being overwritten by an absent registry binding.

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

* fix(runner): honor orphan reap when adopted PID lacks a start marker

finishWithOrphanSweep with an unknown base lets a successful argv kill
report stopped instead of forcing unknown after the marker probe fails.

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

* fix(hub): confirm archive via metadata.hostPid tombstone

When StopSession(sessionId) is unknown and KillSession supplies no
confirmable pid+marker, probe PID-<metadata.hostPid>. already_gone
allows archive; still_alive/unknown still refuse (#1911 Peer #1820).

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

* fix(cli): escalate Windows taskkill to /F like SIGTERM→SIGKILL

Soft taskkill /T is refused on win32 console trees; orphanReap left
orphans still_alive. waitForProcessToDie now force-escalates on both
platforms so cross-platform reaping matches.

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

* fix(cli): widen process.kill mock signal to string | number

Match @types/node process.kill(pid, signal?: string | number) so the
Windows taskkill escalation mocks typecheck on tip 54f141368.

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

* fix(cli): tasklist liveness + immediate /F after soft taskkill refuse

Windows orphan reap still failed in 5ms on Teemo: soft taskkill refused
but escalate waited on an unreliable process.kill(0) probe. Prefer
tasklist for win32 liveness and escalate to /F as soon as the PID is
still live after soft kill.

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

* fix(cli): coerce win32 ps-list PIDs and escalate soft taskkill to /F

String PIDs from ps-list made Number.isFinite fail so orphan kill
returned still_alive without calling taskkill. Coerce PIDs, prefer
tasklist liveness, and escalate soft taskkill to /F like SIGTERM→SIGKILL.

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

* fix(cli): Windows orphan scan via CIM CommandLine + taskkill /F escalate

ps-list/fastlist is missing CommandLine and often missing from single-exe
bundles, so Teemo orphan reap returned scan_failed (logged as live PIDs).
List via Win32_Process CIM, coerce PIDs, escalate soft taskkill to /F.

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

* fix(cli): grace-wait after successful soft taskkill before /F

Escalate immediately only when soft taskkill is refused. When soft
succeeds but the PID is still draining, honor the existing grace period
before force (Codex Major on #1911).

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

* fix(runner): re-verify start marker before orphan argv tree-kill

Capture getProcessStartMarker after the argv match list, re-check
immediately before killTree, and skip on mismatch/null-while-alive so
PID reuse cannot tree-kill an unrelated process on the orphan fallback.

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

* fix(hub): preallocate HAPI id before fresh machine spawn

Fresh POST /machines/:id/spawn omitted existingSessionId, so argv never
got a reap stamp. Preallocate the hub row (fork/clear pattern) and pass
that id so a runner restart before the first webhook remains reapeable.

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

* fix(cli,hub): close three post-prealloc unreapable paths

Pass --hapi-session-id into Claude/cursor/kimi/copilot bootstrap so the
preallocated hub row is used. Treat null marker re-check while alive as
still_alive (not stopped). Keep the preallocated stub when spawn-fail
StopSession cannot confirm the child is gone.

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

* fix(cli): adopt-stub via reservedSessionId, not existingSessionId

`--hapi-session-id` binds a fresh create bootstrap to a hub-preallocated
UUID (getOrCreateSession id) without taking the reopen path. Non-UUID
stamps stay reap-only to match hub create schema. Restores integration
spawn invariants broken by mapping hapi→existingSessionId.

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

* fix(cli,hub): close Opus cold-read B1–B3 for #1911

B1: capture start markers from the same process snapshot as argv
match (POSIX ps lstart / Win32 CreationDate); re-check before kill.
Continue other orphans when one target is unconfirmed.

B2: Windows killProcessTreeByPid verifies every collected descendant,
not root-only — taskkill /T exit 0 is signalled, not gone.

B3: runner sets childStarted:false on pre-exec rejection; hub deletes
the preallocated stub without StopSession. Post-exec ambiguity still
keeps the row until stop confirms. Terminal archives no longer refuse
when no runner is connected.

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

* fix(cli,hub): explicit adopt for hub-preallocated session stubs

Fresh machine spawn preallocates with tag machine-spawn:<uuid>; CLI
create with a different tag and the same id 409'd on identity conflict.
Add adoptPreallocatedSession (hub-internal stub check) and POST
/cli/sessions { adopt: true, id } so create binds the reserved row and
overwrites tag/metadata without guessing the hub tag. Tests fail-first
on the real prealloc→CLI create path.

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

* fix(cli): preserve snapshot markers + unify Windows CIM start markers

Runner stopSession was still on findOrphans (PID-only), discarding same-snapshot
markers and reopening the PID-reuse kill window. Switch both call sites to
findTargets. Force UTC ISO 'o' CreationDate for list and single-PID probe so
ConvertTo-Json cannot disagree with getProcessStartMarker on WinPS 5.1.

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

* fix(cli): exercise production orphan sweep wiring in tests

Extract findStopSessionOrphanTargets as the stopSession discovery path both
run.ts sweeps call. Add tests that drive that helper end-to-end (not an
injected findOrphans seam), assert run.ts source wiring, and cover CIM-list
ISO vs probe ISO agreement plus the broken WinPS shape mismatch.

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

* fix(hub,cli): never delete live spawn on RPC timeout; release stub tags

Ambiguous machine-spawn failures (timeout / unset childStarted) no longer
call stopRunnerSession or deleteSession — that path killed healthy late
boots and CASCADE-wiped transcripts. Pre-exec delete still requires a real
machine-spawn stub (store tag check). Metadata updates release the stub
tag so reopen flavors are not permanently adoptable; adopt refuses
archived stubs. POSIX pgrep ENOENT fails closed instead of root-only verify.

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

* fix(cli): parseable Windows tree scan + fail closed on CIM failure

Space-joined PowerShell for collectWindowsProcessTree was a WinPS parse
error ($seen=@{} $bfs=@()), so every scan fell open to [root] and B2
false-stopped again. Join with newlines; return scan_failed on error/empty;
killProcessTreeByPid refuses root-only verify. Assert on generated command
text (Teemo-confirmed) so mocks cannot hide the next malformed script.

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

* fix(cli,hub): reopen stamps existing-session-id; scan-fail signals root

Opus Critical on #1911: runner always stamps --existing-session-id for
known hub ids so Claude/Kimi/Copilot reopen does not hit adopt-stub 409.
On pgrep/CIM tree-scan failure still signal the verified root but never
claim stopped. Archive when both KillSession and machine RPC are missing.
Empty Win process-list stdout and strict Codex registry reads fail closed.

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

* fix(cli,hub): OK: sentinel for Windows tree scan; adopt CAS

CIM failure under SilentlyContinue printed root-only with exit 0 —
byte-identical to a healthy childless tree, so scan_failed never fired
and killProcessTreeByPid false-stopped (#1911 Overseer B1). Require
ErrorActionPreference=Stop, trap exit 1, and an OK: success sentinel;
parse anything else as scan_failed. Same Stop/trap on the machine-wide
list command. Execute the generated scripts under Docker pwsh in CI
when the image is present. Treat pgrep status=null as scan_failed;
coarsen Windows tasklist poll; wrap adoptPreallocatedSession in a
transaction with stub-tag CAS.

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

* fix(cli,hub): reservedSessionId for fresh prealloc; signal Win survivors

Fresh machine-spawn stubs must not stamp --existing-session-id — that
sends Codex down reopen without a thread binding (#1911 bot Major) and
re-broke local HTTP non-UUID hints as reopen (round-4). Hub passes
prealloc as reservedSessionId; buildCliArgs stamps --hapi-session-id
for reserved/sessionId and --existing-session-id only for live reopen.
Codex parses --hapi-session-id and adopts via bootstrapSession.

Windows tree kill now taskkills surviving pre-scan PIDs after /T misses
broken intermediate links.

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

* fix(cli,hub): fail-loud Docker pwsh CI; scope Codex registry; stub leak

CI skipped all four windowsProcessTree.pwsh tests on 48bb (image inspect
without pull). Pull the image in test.yml and throw under CI when missing.
Scope strict runtime-registry failure to Codex stop contexts so one bad
JSON cannot block archive machine-wide. Stamp childStarted:false on
machine RPC outside_workspace_roots. Add Codex reservedSessionId prealloc
and runSharedCodex regression coverage for the #1911 Critical.

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

* fix(cli,hub): single-use reservedSessionId; skip UUID HTTP hints

Consume the machine-spawn reservation on first prepare() so a second
Codex root/fork cannot re-adopt a consumed stub (409). Stop stamping
UUID local-HTTP sessionId as --hapi-session-id (adopt hard-fail). Update
stale hub spawn mocks for the reservedSessionId arity.

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

* fix(hub): archive never-started machine-spawn stubs on unknown

Ambiguous keep-stubs (startedBy=runner, no hostPid, still machine-spawn
tagged) made StopSession return unknown forever while the runner was
online, so archive threw permanently. Treat unknown + prealloc stub +
no hostPid as already_gone. Also reject spawn success when the runner
reports a different id than the preallocated stub.

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

* fix(hub,cli): refuse dual-RPC-miss archive; generation-check Win survivors

Both KillSession and StopSession missing is not proof a detached CLI exited —
keep the row unconfirmed until the runner reconnects. Windows tree kill now
re-checks process-start markers before individually signalling survivors so
PID reuse is not killed.

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

* fix(cli): refuse archived reopen in bootstrapExistingSession

Hub-archived rows could be resurrected when a late-booting child hit
--existing-session-id: buildSessionMetadata stamped lifecycleState=running
and updateMetadata overwrote the archive. Adopt already rejected archived
stubs; reopen now matches. Intentional revive still goes through hub
reopenSession first. Correct stub-hatch comment (no post-archive argv sweep).

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

* fix(hub,cli): reject un-archive in updateSessionMetadata CAS

Hub-side transactional guard (Discovery #1911 M1): refuse metadata writes
that clear archived lifecycle unless allowUnarchive (reopen clear). Client
stops infinite updateMetadata backoff when mismatch applies archived.
Clear archive immediately before spawn so Pi/PTY attempt snapshots stay valid.

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

* fix(hub,cli): success+preserve hub archive; M3 reservedSessionId split

Replace version-mismatch refuse (livelock in CAS backoff) with success +
merge-preserve of hub-archived fields unless allowUnarchive. Ack path
routes through noteHubArchived so the CLI exits (criterion 6). Clear-
before-spawn remains the authorized revive. Thread --hapi-session-id to
reservedSessionId for shared-parser flavors (adopt≠reopen).

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

* fix(cli): fail-closed orphan ps signal; PID- registry; spawn timeouts

Overseer B1: signalled ps (status null) is scan_failed, not empty table.
B2: unreadable registry on PID-* stops returns null (no soft []).
B3: 10s timeout on process/orphanReap spawn.sync. Narrow bootstrap
archived belt to archivedBy=hub to match hub merge-preserve.

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

* fix(cli,hub): hubArchived latch; PTY archiveSnapshot durability

Criterion 6: noteHubArchived latches hubArchived so late
registerKillSessionHandler registrations still EXIT (EventEmitter
does not replay). Mirror Pi: persist archiveSnapshot on
ptyResumeAttempt across clear-before-spawn and quarantine, restore
on failed-resume cleanup (#1911 Discovery Majors).

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

* fix(cli): Codex hub-archived exit; AC6 latch test production order

C2: SharedCodexRoot registers hub-archived (and latch check) beside
its private KillSession — Codex never called registerKillSessionHandler.
C1: keep latch+subscribe; test write-then-register order Discovery proved.

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

* fix(cli): type Codex hub-archived end mock for tsc

vi.fn(async () => {}) inferred zero-arg calls; RootHost['end']
signature unblocks end.mock.calls[0][0] (TS2493).

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

* fix(cli): catch hub-archived Codex host.end rejection

void this.host.end() discarded archive-RPC failures as unhandled
rejections; Codex has no unhandledRejection handler, so Node can
exit the shared wrapper and kill sibling roots mid-turn.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-25 09:15:08 +01:00
SSU-WEI HUANGandGitHub d332cd5957 fix(acp): preserve permission state when tool input is missing (#1782) 2026-09-09 09:22:27 +08:00
weishu e5a8212f4a feat(session): validate agents and browse workspace directories 2026-08-25 16:12:29 +08:00
SSU-WEI HUANGandGitHub b744414241 fix(cli): skip invalid set_mode for interactive Copilot sessions (#1463)
* fix(cli): skip invalid set_mode for interactive Copilot sessions

applyInitialAgentMode() unconditionally sent session/set_mode with
modeId 'interactive', which the Copilot ACP server rejects (Invalid
mode 'interactive' - supported: agent/plan/autopilot), killing every
default-mode Copilot session at startup.

- Short-circuit applyAgentMode('interactive') as a no-op success,
  mirroring buildCopilotAcpArgs which already omits --mode at spawn
- Classify 'Invalid mode' responses as unsupported runtime switching so
  server-side mode rejections degrade gracefully (restart to apply)

Verified: bun typecheck + bun run test (full suite green).

* fix(cli): map Copilot interactive mode to ACP agent mode

Address review findings: the previous no-op for interactive left the
backend in Plan/Autopilot after a runtime switch while HAPI reported
Interactive, and classifying 'Invalid mode' responses permanently
disabled switching for valid modes too.

- Map interactive -> the ACP 'agent' mode id (the spawn default) so
  session/set_mode always receives a valid mode and runtime switches
  back to Interactive stay in sync with the backend
- Keep 'Invalid mode' out of the capability-loss classifier: it is a
  per-mode rejection, not evidence that set_mode is unavailable
2026-08-10 10:44:24 +08: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
f10fbc7496 feat(cli): add GitHub Copilot CLI agent support via ACP (#1245)
* feat(cli): add GitHub Copilot CLI agent support via ACP

Wrap `copilot --acp --stdio` for remote sessions and spawn the native TUI locally, with full hub/web integration for spawn, resume, and permissions.

Fixes tiann/hapi#362

Co-Authored-By: HAPI <noreply@hapi.run>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(copilot): agent modes, models, slash/file UX, local session sync

Add Interactive/Plan/Autopilot (fleet is slash-only), subscription-aware
model discovery, web StatusBar/permission UX, @ file mentions, and fix
local Safe Yolo plus session-id locator for handoff/resume.

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

* chore: re-trigger Codex PR review after auth outage

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

* chore: retry Codex PR review

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

* fix(copilot): preserve agent mode on resume and apply via ACP set_mode

Resume was dropping copilotAgentMode so Plan/Autopilot reset to interactive.
Also switch local/remote mode application to --mode / session set_mode instead of slash prompts.

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

* fix(copilot): wake remote loop when agent mode changes

Empty isolated queue tick lets setMode apply without inventing a user prompt.

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

* fix(copilot): confirm mode changes before persisting

Await Copilot mode changes and expose discovered models so session state reflects backend acceptance.

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

* fix(copilot): guard mode discovery and slash updates

Keep model probes within runner roots and preserve active sessions when mode switching is unavailable or rejected.

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

* fix(copilot): preserve resume and auto semantics

Deduplicate Copilot resume rows, apply Auto explicitly, and fail closed on denied permissions.

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

* fix(copilot): close permission and model discovery gaps

Keep write-capable commands pending in read-only mode, extend model probe RPCs, and preserve explicit model validation before session creation.

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

* fix(copilot): persist runtime model and agent mode

Fallback to ACP model options when direct model switching is unavailable and retain Copilot agent mode across hub restarts.

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

* fix(copilot): normalize composer auto selection

Use the null session sentinel for Copilot Auto so the composer selects and resets default models consistently.

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

* fix(copilot): reject local permission mode changes

* style(copilot): remove trailing blank line

* fix(copilot): secure local config handoffs

* fix(copilot): reject local agent mode slashes

* fix(copilot): reject mode changes during turns

* fix(copilot): consume rejected slash updates

* fix(copilot): preserve thinking across slash handling

* fix(copilot): stabilize async config changes

* fix(copilot): roll back rejected startup model

* fix(copilot): preserve cancellation and file mentions

* fix(copilot): hide local permission controls

* fix(deps): support clean workspace installs

* test(copilot): account for spawn mode argument

* fix(copilot): attribute usage to active model

---------

Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 08:19:12 +08:00