Hub
- independent teams.db (own user_version ladder); feature gate default OFF,
so a disabled hub never creates the file and team routes simply do not exist
- /api/teams: list/detail/by-session/memory/messages/spawn/tasks,
human message channel, team rename/archive/delete
- team runtime: spawn members via the existing runner RPC (worktree/yolo
options), peer message routing (mention/dm push, broadcast pull-only),
budgets (max members, message rate, reply chain depth), member-offline
announcements that wake the lead or escalate to the human
- team-attention notifications: in-app toast when visible, web push otherwise
- team memory dir {HAPI_HOME}/teams/<id>/ with charter.md template; read-only
browse API with path-traversal guards
CLI
- MCP tools: team_status / team_read / team_send / spawn_peer (claude, codex
bridge and ACP flavors); spawn_peer stays behind manual approval
- HAPI_TEAM_* env for spawned members; system-prompt block for Claude/Codex;
assignment brief carries team rules for flavors without prompt injection
- RUNNER_CAPABILITIES.agentTeam advertised to the hub
Web
- team group chat route with folded peer threads, key/task filters,
human composer, member/decision/system message cards
- side panel: live member status, interactive task board (create/status/
assignee), team memory browser, budget
- team create / add-member / settings dialogs; sidebar team section;
hub settings toggle for the feature (restart to apply)
- SSE team-updated invalidation
Zero impact when disabled: no teams.db, no routes, no tool or prompt changes.
Revert 8357da0a9d and its later shared-runtime integration.
Remove automatic model fallback, account usage polling, and the related UI, protocol fields, tests, and documentation without adding replacement quota handling.
Keep generic model pagination and method probing required by the current shared-session architecture. Cover idle sessions staying online without usage polling or agent-state churn.
Use one native app-server for terminal, Web and phone clients while retaining the existing CLI and Runner lifecycle.
Synchronize native queues, permissions, question history and steering state; preserve explicit permission precedence and per-turn usage models. Resume inactive clear commands through Runner and reject independent child cold resumes.
Add shared-runtime regression tests, generated protocol fixtures and lifecycle documentation.
Vitest's 5s default flakes under GHA Defender/cold-start on the only
unit test that real-spawns on Windows (#1823). Keep the global unit
default unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(codex): ignore late app-server writes during disconnect
* fix(codex): scope app-server responses to their process
---------
Co-authored-by: huxiang <huxiang@myai.tech>
* 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)
* 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
* 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(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(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(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
* 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(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(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
* 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(steer): persist indeterminate outcomes without replay
* fix(steer): make ambiguous delivery restart-safe
* fix(steer): recover crash-held rows and preserve retry dedup
* fix(steer): ack retries and bound stdin dispatch
* fix(steer): reconcile indeterminate dispatches and serialize retries
* fix(codex): classify stdin callback failures as indeterminate
* fix(steer): recheck indeterminate cancels after ACK
* fix(steer): close retry and abort races
* fix(steer): serialize live retries and abort admission
* fix(steer): distinguish live dispatching from unknown
* fix(steer): keep ACK failures held and reconcile busy cancel
* fix(steer): distinguish held cancel from removal
* fix(store): combine schema v24 migrations
* fix(store): reserve schema v25 for steer delivery state
* fix(steer): keep held cancel state and notify requeue
* fix(steer): release explicitly cancelled unknown reservations
* fix(codex): reject cancelled reservations before native steer
* fix(codex): make reservation restore atomic with state
* fix(codex): terminate abandoned transport writes
* fix(steer): own abandoned app-server lifecycle and consume races
* fix(codex): confirm dispatch and recover abandoned turns
* test(codex): mock abandoned transport callback
* fix(codex): clear visible turn state on transport loss
* fix(steer): claim retries and cover native delivery state
* fix(native): preserve indeterminate state on Android hydration
* fix(steer): make retry claims single-winner
* 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(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(codex): preserve native instructions
Keep HAPI guidance in the developer layer so app-server retains Codex persistence rules. Align long-running goal states with the Codex 0.147 protocol.
* fix(codex): filter new goal statuses
* 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>
* feat: settings toggle for AGENT_NOTIFY_SUMMARY contract injection
Add a hub-persisted, default-off Settings control so operators can opt agents into emitting the trailing AGENT_NOTIFY_SUMMARY line. Propagate the resolved flag on CLI session bootstrap and inject at call time for Claude, Codex, OpenCode, and Grok (Cursor still unsupported).
Closes#1375
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: lock hub settings RMW and restore abort deliveryMode
Serialize settings.json updates with the shared .lock protocol and unique
temp files so the new hub toggle cannot clobber CLI/relay fields under
concurrency. Also supply deliveryMode on abort send-error restore so web
typecheck (and CI) pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): wrap SettingsGeneralPage tests with QueryClientProvider
The hub-settings toggle uses TanStack Query; the settings page suite
was rendering without a QueryClient and blew up CI.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): defer summary-contract toggle until settings load
Avoid rendering an interactive false switch while the hub GET is still
in flight, which could overwrite an enabled preference on early click.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): clarify Grok coverage for summary-contract toggle
Local Grok has no instruction inject path; settings copy now matches
remote-only Grok support (and still excludes Cursor).
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Normalize usage input at parse time, mark inclusive producers, rebuild derived usage indexes, and preserve valid primary usage when cache partitions are malformed.
Fixes#1389
* feat(cli): MCP list_peers + runner hub auth inheritance
Runner-spawned agents could not discover same-hub peers without
sitting on the hub host or pasting a session id. Add MCP list_peers
(in-process credentials), export HAPI_API_URL/CLI_API_TOKEN after
auth init for shell fallbacks, and clearer auth failure hints.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): do not export default hub URL into HAPI_API_URL
exportHapiHubAuthEnv was writing the implicit localhost default into
process.env, which made maybeAutoStartServer skip starting the bundled
hub. Only export HAPI_API_URL when the URL came from env or settings;
always still export CLI_API_TOKEN. Also fill missing deliveryMode on
abort restore so web typecheck matches RawSendError (main tip unblock).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): widen initializeApiUrl mock return type in test
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): never export CLI_API_TOKEN; exclude self from list_peers
Keep settings/prompt-backed hub secrets out of wrapped agent env so
shell JWT+curl cannot bypass peer-tool approval. Fresh hapi re-reads
settings; env-backed tokens already inherit. list_peers omits the
calling session from the shortlist.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): resolve peer labels via summary/path like web titles
list_peers was showing (unnamed) for ordinary sessions because titles
live in metadata.summary.text. Match web getSessionTitle and collapse
whitespace so each peer stays one agent-readable line.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli,hub): emit full peer ids and honor GET /sessions?limit
Short 8-char prefixes collide across UUID namespaces; print full ids so
resolveSessionByPrefix stays unambiguous. Honor optional limit after sort
so listPeerSessions stops loading the whole namespace for scheduled counts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub): type sessions limit test mock as Map<string, number>
CI tsc rejected Map<string, null> for getNextScheduledAtBySessionIds.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli,hub): unbounded ping resolve; peer list order=updatedAt
Keep GET /sessions?limit only for discovery callers. ping/inspect omit
limit so full UUIDs outside the first 500 stay resolvable. Peer lists
pass order=updatedAt so truncation matches newest-first. Basename
fallback splits Windows paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): auto-approve ACP title List Peer Sessions
Permission derivation prefers request.title; match the MCP tool title
form so default-mode ACP sessions do not prompt on discovery.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): pad list_peers fetch; split hub URL vs token hints
Fetch limit+2 when excluding the caller so overflow still surfaces at
limit=100. Clarify that auth login only saves the token, not HAPI_API_URL.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): use boolean overflow for ping-peer --list
Match MCP list_peers: fetch limit+1 and mark hasMore instead of claiming
an exact omitted count from a 200-row sample.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub): tolerate mocked machineCache without expireInactive
CI flake: 5s inactivity tick hit test doubles that only stubbed
getOnlineMachinesByNamespace. Optional-call + stub the method.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(a2a): steer session citations toward inspect_peer
Copy-reference prose and markdown /sessions/<id> links both parse to hub
ids; MCP/CLI descriptions and flavor prompts forbid treating them as local
FS paths so agents call inspect_peer first (tiann/hapi#1370).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(a2a): fail closed on ambiguous session citations
Codex #1373: do not silently pick ids[0] when a paste contains multiple
/sessions/ links (shared by inspect_peer and ping_peer). Also strip
trailing prose punctuation from bare citation ids.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(a2a): prefer Copy-reference path over title /sessions/
Codex #1373 MINOR: titles containing /sessions/<other> must not make
normalizeSessionIdPrefix fail closed on an otherwise valid paste.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(a2a): do not short-circuit multi-citation Copy-reference pastes
Codex #1373 MAJOR: only treat parenthesized Copy-reference as canonical
when the paste is that citation alone (plus optional steer suffix).
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix codex stale terminal recovery
* fix(codex): ignore stale retry failures
* fix(codex): ignore stale retry terminal failures
Only task completion may bypass stale-turn duplicate handling during same-thread recovery, preventing delayed failed events from finalizing the active retry.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(codex): separate stale turn recovery guard
Limit matching-thread status events to missing turn IDs so delayed status failures cannot affect an active retry turn.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(codex): scope stale completion recovery turn
Accept a stale completion only for the immediately finalized turn, so older retries cannot finalize the active turn.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* 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>
* feat: add message-level conversation fork and rewind
Expose native Codex/Grok/Claude history controls through hub REST+RPC and web ConfirmDialog actions, without file rewind or composed forks. Also reconcile the duplicate hub V14→V15 migration so typecheck can pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: hydrate fork transcript and consume Claude --fork-session
Forked HAPI children now copy the source transcript prefix so navigation is not a blank thread, and Claude drops --fork-session after the first launch so relaunches do not branch again.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: harden fork/rewind concurrency and durable history points
Skip pending scheduled rows when hydrating fork transcripts, serialize fork/rewind per session, and persist conversation history points/indexes across existing-session bootstrap and Grok relaunches.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: close remaining fork/rewind races and UI anchoring
Block sends and scheduled maturation while history actions run, order fork prefixes by invocation time, inherit history locators into children, and only offer Fork current on the live tail boundary.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: address remaining fork/rewind bot findings
Materialize Claude --fork-session before the first child prompt, validate
HAPI history boundaries before native RPC, expose forkCurrent on a latest
user boundary, fully demote unsupported conversationHistory capabilities,
and fix the truncate test setup order.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: align fork-current ids and Claude fork bootstrap
Compare the latest fork boundary in assistant-ui threadMessageId space,
spawn Claude forks with the persisted session mode, and preserve
forkedFrom across existing-session bootstrap.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: close fork/rewind consistency holes at the contract layer
Hold the source history lock until Claude child binds a distinct native
id, persist Codex localId→turnId locators, and mark/block diverged
sessions when native rewind outruns HAPI truncate.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: use Codex stable lastTurnId for historical fork
Map HAPI's exclusive boundary to the previous turn's inclusive
lastTurnId so native fork context matches the hydrated transcript.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: require exact Grok native resume for fork children
Reject newSession fallback when forkedFrom is set, and keep the hub
history lock until the child binds the forked grokSessionId.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: kill active fork children before failed-fork cleanup
Bind/readiness failures can leave the child process running; deleteSession
rejects active rows, so terminate first then remove the HAPI session.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: close remaining fork lock, hydrate, and todos gaps
Reject mode switches during history actions, batch-copy fork
transcripts in one SQLite transaction, and rebuild todos after
fork hydrate / rewind truncate.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: allow Codex historical fork before the first turn
Use experimental beforeTurnId when there is no previous turn for the
stable inclusive lastTurnId boundary.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: mark Grok history busy immediately after dequeue
Hub idle checks clear once messages-consumed fires; hold the busy flag
across permission sync and rewind-points lookup before prompt starts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: encode copied conversation history content
* fix(web): hide local conversation history actions
* style(codex): remove trailing whitespace
* fix(fork): preserve children when cleanup is unconfirmed
* fix(history): confirm cleanup and guard rewind divergence
* fix(history): probe capabilities before advertising
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(codex): support /personality via in-session override
Intercept /personality in the Codex slash layer, keep the value in CLI
memory only, and forward it on thread/turn start when set. Unset means
omit the field so Codex config/thread defaults apply—no Hub DB or web UI.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(codex): refuse fake /personality clear (sticky thread setting)
turn/start.personality sticks for later turns; omitting the field does not
restore config.toml. Drop default|auto|clear success paths and require an
explicit friendly|pragmatic|none instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): feature-flagged rich composer for inline session @ mentions
Custom segmented contenteditable (not TipTap) inserts caret-local session
atoms from the existing @ picker and serializes to markdown links on send.
Textarea path remains default until flag parity dogfood.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): rich composer mention boundary + #1215 refs
Treat U+FFFC mirror atoms as word boundaries so @ after a session
token still opens autocomplete. Point comments at Fixes#1215.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(web): peer-stack e2e for rich composer session @ mentions (#1215)
Smoke: flag on, @ picker inserts inline session atom chip (not prose dump).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): preserve newlines in rich composer Enter-newline mode
Chromium splits contenteditable on Enter into block divs; serialize those
as \\n and insert <br> when parent leaves Enter unhandled (Shift+Enter /
enter-inserts-newline).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): show @ badge when rich composer mentions flag is on
Dogfood was invisible: flag-off looks like a normal textarea, and flag-on
had no chrome. Surface a small @ badge when enabled.
* fix(web): rich session composer on by default (not a user setting)
The plan dual-path was an engineering kill-switch, not an opt-in. Default
to the segmented composer; only richMentions=0 disables. Drop the flag
badge and record a peer-stack motion proof covering chips + baseline UX.
* fix(web): make rich composer Shift+Enter create a visible newline
Trailing <br>+empty text node was a silent no-op at EOL. Use
insertLineBreak (ZWSP pad fallback), assert real \\n in peer e2e.
* feat(web): hover tooltips on rich composer session chips
Show full title, status, short id, and path on chip hover via a portal
bubble fed by live useSessions lookup (drafts fall back to title + id).
* fix(web): dismiss rich composer chip tooltips on mouse leave
contenteditable pointerout/relatedTarget was flaky so tips stuck after
leaving the chip. Hit-test on pointermove, clear on prose/input/leave.
* fix(web): address cold-review Blocker/Majors on rich composer
Exclude peer e2e from default Playwright; force plain-text paste; restore
newline hard-stop in findActiveWord; fix root-anchored selection mapping
and nested-block serialize; cover with unit tests.
* chore: drop accidental .cursor files from rich-composer tip
* fix(web): close remaining cold-review gaps on rich composer
Drop absolute peer e2e tooling imports, prove chip→markdown send, and
harden paste/EOL/focus/tooltip/Enter edges before Meta rematerialize.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: absorb soup playwright.config union for clean remat
Keep fork peer-stack timeouts/annotated-video wiring and add testIgnore
for e2e/peer so the next driver rematerialize does not conflict.
Co-authored-by: Cursor <cursoragent@cursor.com>
* revert: drop fork playwright tooling from upstreamable tip
Peer-stack annotated-video + HAPI_PEER wiring stay on fork main / soup.
Product tip only needs testIgnore for e2e/peer (see docs/tooling/peer-stack.md).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): fix rich composer Shift+Enter double newline and paste space
Prefer manual newline+pad over execCommand insertLineBreak, and stop
applying autocomplete trailing-space on paste/drop paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): pad EOL Shift+Enter after Range.insertNode split
insertNode always leaves an empty text sibling, so !nextSibling never
saw EOL; detect meaningful trailing content and cover with jsdom tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): drop custom onDrop from rich composer
Intercepting drop without caretRangeFromPoint landed text at EOF or
no-oped in-editor moves. Native CE drop is enough for #1215; paste
still forces plain text.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): sidebar-parity tooltips on rich composer session chips
Reuse SessionRowSummary (flavor, thinking/attention, schedule, todos,
relative ago, path) for chip hover so the tip matches the session list.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: keep peer-stack e2e off the upstreamable tip
Peer specs and playwright.peer.config stay on fork main per
docs/tooling/peer-stack.md; default config still testIgnore's e2e/peer.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: cite sessions with UUID wire + inspect_peer for agent/overseer
Rich composer chips already serialize to [title](/sessions/<id>); flush
before send so the agent prompt never gets title-only chip text. Add
inspect_peer (MCP + hapi inspect-peer) as the read twin of ping_peer so
that same id is immediately usable for overseer/agent peer lookup, with
system-prompt glue from citations to inspect/ping.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): gate inspect_peer behind permission approval
Cross-session history reads need the same prompt path as ping_peer:
keep inspect_peer off Claude --allowedTools and treat it as sensitive
in ACP/OpenCode read-only mode so prompt injection cannot silently
enumerate peer transcripts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: clarify playwright peer testIgnore is upstream-safe
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): keep session UUIDs on rich composer copy/cut/paste
Copy/cut write wire markdown so chips do not collapse to @title-only
clipboard text; paste reparses session links back into atoms.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(cli): add ping-peer CLI and MCP ping_peer for peer messaging
Promote resume-if-inactive + wait-active + POST message into a first-class
CLI command and session MCP tool so agents stop reinventing JWT+curl.
Fixes#1194
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): do not auto-approve MCP ping_peer
Cross-session messaging can resume a peer and inject a prompt, so keep
permission-mode gating (Codex PR review Major on #1195).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): require approval for ping_peer in read-only mode
Read-only auto-approve treated non-write names as safe; ping_peer can still
resume a peer and inject prompts, so gate it like a write tool.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): keep ping_peer out of Claude --allowedTools
toolNames still registers the MCP tool, but Claude auto-allow must not
pre-approve cross-session resume+inject without a permission prompt.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): re-check session active before ping-peer send
List/get can race; POST /messages still 409s if the target flips inactive
before send. Resume+wait again (and re-gate pi) immediately before POST.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(cli): export HAPI_SESSION_ID into wrapped agent env
Publish the hub session id into process.env at session bootstrap so every
downstream agent spawn inherits it. HAPI runs one hub session per CLI process
(the runner forks a fresh hapi child per session; local is 1:1) and every
flavor's agent spawn derives its child env from process.env, so a single seam
covers claude / codex / cursor / gemini / opencode / kimi / grok / pi -
runner-spawned and local - plus future flavors, without touching each launcher.
Agents can read HAPI_SESSION_ID to self-target their own hub session over REST
or shell helpers without listing /api/sessions. Prefer the MCP display_image
tool for inline media when available; HAPI_SESSION_ID is the deterministic
fallback for non-MCP tooling.
Closes#1119
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(scripts): self-target hapi-display-image via HAPI_SESSION_ID
Teach the in-tree shell helper to use $HAPI_SESSION_ID for path-only /
self invocations: GET /api/sessions/:id directly instead of listing
/api/sessions. Explicit session prefixes keep the previous list path.
Gives #1119 a tangible now benefit - the tool that forced the wasteful
list-and-reverse-lookup dance no longer needs it inside a wrapped session.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): defer HAPI_SESSION_ID export until lazy Codex materializes
The provisional lazy-session id was exported at bootstrap before the hub
row existed, so path-only self-targeting (GET /api/sessions/:id) could
404 while materialization was still pending. Export on onMaterialized
instead, and await materialize in buildHapiMcpBridge before starting the
MCP server / spawning Codex so the agent inherits an id the hub can
resolve (and so hapiMcpUrl is persisted, not only local pending state).
Addresses Codex review Major on #1121.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>