* fix(cli): drop unknown SDK message types instead of passing them through
The SDK-to-log converter's switch had a fail-open default that stamped any
unrecognized SDK message with transcript base fields (parentUuid/sessionId/
userType) and forwarded it. Claude Code emits a tool_progress heartbeat every
30s for long-running tools, so a single slow Bash call flooded the chat: the
web normalizer matches no known shape for those records and falls back to
rendering the raw envelope as message text.
Gate the switch on an explicit allowlist instead, bailing before the uuid is
allocated so a dropped event cannot advance sidechain/parent tracking -- the
heartbeats share one parent_tool_use_id and were overwriting the pointer a
subagent's next real message parents to. This matches the local launcher,
which already enforces the same allowlist via RawJSONLinesSchema.safeParse.
The default branch stays as a fail-closed guard so adding a type to the
allowlist without a matching case drops the message rather than leaking it.
* fix(cli): re-check reassembled text for internal event JSON at flush boundary
isInternalEventJson was only applied per incoming chunk. In delta mode
(OpenCode) every chunk is a fragment, so none of them parses as JSON on its
own and the filter never fires; the pieces accumulate and flushText emits the
reassembled envelope verbatim. The dedupe path has the same hole whenever two
chunks share no overlap.
Check again in flushText, which is the first point the complete text exists,
and tolerate surrounding whitespace so an envelope preceded by a newline is
not waved through by the leading-'{' fast path.
Genuine assistant output that happens to be JSON is unaffected: the matcher
still requires the specific { type: 'output', data: { parentUuid, sessionId,
userType } } envelope shape.
* fix(cli): fail closed on unrecognized agent message in converter
convertAgentMessage's exhaustiveness default returned the message object
itself at runtime. The never binding makes the branch unreachable today, but
every caller forwards a non-null result straight into the chat stream, so the
failure mode if it were ever reached is a raw object on screen. Keep the
compile-time check, return null at runtime.
* test(cli): cover command_lifecycle, a second unknown type seen leaking
Observed in the same session after tool_progress. The allowlist already
covered it with no code change, which is the argument for gating on known
types rather than adding a case per offender.
* 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>
* fix(web): preserve composer attachments across session switches
Persist composer files per session and restore completed uploads without
uploading them again. Clear attachment drafts alongside text after send and
cover restoration, isolation, and adapter reuse with regression tests.
Fixes#465
* fix(web): keep cleared attachment drafts tombstoned
Retain an empty in-memory cache entry until the queued IndexedDB delete completes so a fast remount cannot restore stale files. Add regression coverage for the clear/remount race.
* fix(web): defer attachment restore for inactive sessions
Only restore or clear attachment drafts while the session attachment adapter is available. Preserve saved files when an inactive session mounts without attachment support and add regression coverage.
* fix(web): use dedicated split breakpoint for compact tablets
Some compact Android tablets (e.g. OPPO Pad mini) report a landscape
CSS viewport below Tailwind's `lg` (1024px) despite having enough
physical screen space, so the sessions layout fell back to a single
column. Add a dedicated `split` breakpoint at 920px and use it for the
sessions split layout and the sidebar width/resize CSS, leaving the
global `lg` breakpoint (and all other pages) untouched.
* fix(web): cap sidebar width against viewport on compact split
A persisted sidebar width (up to 600px from resizing on desktop) could
shrink the detail pane to 316px at the new 920px split breakpoint, below
the previous 1024px worst case of 420px. Cap the sidebar width at
min(var(--sidebar-w), calc(100vw - 424px)) so the detail pane keeps at
least 420px down to 920px, with no effect on desktop.
* fix(web): seed sidebar drag from rendered width
When the compact-split viewport cap renders the sidebar narrower than the
persisted width, dragging the handle to shrink it had a dead zone until
the stored width fell below the rendered width. Seed the drag from the
sidebar's rendered width so it responds immediately; unchanged on desktop
where rendered and stored widths match.
The mermaid fallback previously showed only the raw source with no reason,
making a supposedly-valid diagram that fails in HAPI (but passes the Mermaid
CLI) impossible to diagnose from the running UI. Every diagnostic was
swallowed: setParseErrorHandler no-op, suppressErrorRendering, parse with
suppressErrors returning false, and an empty catch.
renderMermaidSvg now returns a { svg, error } outcome. On a failed
suppressErrors parse it re-parses once (no side effects: parse-error handler
is a no-op, suppressErrorRendering stays on) purely to capture the thrown
reason; render() failures capture their message too. The fallback surfaces
that reason plus a data-mermaid-error attribute for automation, while keeping
the raw source verbatim. The failure notice is gated on an actual error so the
async load window does not flash a false 'could not render' banner.
Preserves the #785/#813 hardening (no error-SVG injection, no crash, no new
XSS surface). Addresses the weak-feedback half of #1117.
Refs #1117
Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): autolink markdown links, inline code, and .mmd file paths in chat
Chat autolinking previously only worked for bare file paths in plain text.
Fancier markdown forms silently produced dead links:
- COMMON_FILE_EXTENSIONS omitted common agent-cited types (mmd, puml, rst,
csv, ini, etc.), so bare diagram.mmd never linked.
- inlineCode nodes were never processed, so `path/to/file.md` never linked.
- explicit [label](relative/file.md) links kept a raw relative URL that the
SPA router treated as a dead route under /sessions/.
Changes:
- Expand COMMON_FILE_EXTENSIONS with justified doc/diagram/config/lang exts;
deliberately exclude TLD-lookalikes (org/com/io) to avoid domain false
positives.
- Autolink inlineCode nodes whose ENTIRE value is a single path pattern match
(whitespace-free, allowlisted ext), wrapping an inlineCode child to keep
monospace. Real code snippets are left untouched.
- Rewrite explicit markdown links whose target is a repo-relative allowlisted
file path into hapi-file: hrefs (aligns with #1113). Preserves label.
Security invariants preserved: shouldLinkPath still rejects abs / ~/ / ../ /
Windows-drive / scheme:// paths; scheme-bearing link urls are left for the
deny-scheme layer; deny-scheme handling untouched.
Refs tiann/hapi#1120
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): don't rewrite explicit links in standalone markdown preview
Codex review (#1142): rewriteFileLinkNode ran on the standalone file-preview
surface too, but that surface has no HappyChatContext so the shared `A` anchor
collapses hapi-file: links to plain text (returns props.children when !chat).
That turned an explicit [label](file.md) link in a README preview from an
anchor into plain text.
Gate explicit-link rewriting behind a rewriteExplicitLinks option (default on
for chat) and disable it for the standalone renderer via new
MARKDOWN_PLUGINS_STANDALONE(_WITH_BREAKS) arrays. Bare-path and inlineCode
autolinks are kept — they were already inert on the standalone surface, so no
behavior change there.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): add Copy reference to session context menu
Refs tiann/hapi#950
Adds a More actions item that copies a cross-session citation
(see session "title" (/sessions/id) for context) instead of a
bare share URL.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): sanitize session titles in copy-reference text
JSON-escape titles and collapse whitespace so arbitrary session
names cannot inject prompt text into cross-session citations.
Addresses Codex review on tiann/hapi#951.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Hub already passes access.sessionId on resume (#1088); Cursor CLI still ignored
it (Codex-only). Parse/pass the flag for cursor and lock in reuse-without-ready-wait tests.
Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): buffer Pi prompts until RPC startup ready
A prompt POSTed immediately after spawn (a supported handoff pattern used
by hapi-ping-peer and intake scripts) could reach `pi --mode rpc` before
its `new_session`/`get_state` startup finished, wedging the turn:
`agent_start` then silence, no tool calls. The socket goes `active` (spawn
success) well before Pi's session is initialized, so `active` is not a
safe ready signal for Pi.
Gate outbound prompt/steer sends behind a startup ready gate on PiSession:
`runWhenReady()` delivers immediately once ready, else buffers FIFO;
`markReady()` fires on the first `get_state` response (the signal that
persists `metadata.piSessionId`, which working callers already wait for)
and drains the buffer in order. A 30s unref'd fallback timer force-drains
if `get_state` never lands, degrading to prior send-anyway behaviour
rather than swallowing the message forever.
Fixes#1143
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): honor cancel-queued-message for buffered Pi prompts
Addresses the MAJOR review finding on the startup ready-buffer: while a
prompt is held behind runWhenReady, the hub can send cancel-queued-message
for its localId. Pi registered no onCancelQueuedMessage handler, so
ApiSessionClient acked removed:false, the hub marked the row invoked, yet
the buffered closure still drained on get_state and fired the cancelled
prompt.
Carry the localId with each buffered send and add
PiSession.cancelBufferedMessage, then register apiSession.onCancelQueuedMessage
so a cancel drops the still-buffered prompt (returns true) instead of
sending it. Once drained to Pi it cannot be recalled — returns false,
matching the other agents' queue.cancelByLocalId best-effort semantics.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
The Codex import button used the same circular-arrow SVG as the
session-list refresh button, making the two adjacent actions look
identical. Switch the import affordance to a download-into-tray icon
to match its 'import sessions' semantics.
Closes#1135
* fix codex session import merge (#1123)
修复 Codex 会话导入合并后列表为空的问题。
Fix Codex session import merge so a session is not detected as a duplicate of itself and deleted during merge.
Co-authored-by: LIUZHIRU <ryuu@fine-net.co.jp>
* fix(web): honor explicit older-history loads
Clear the initial scroll-settling window before Load older, outline
load-more, and outline target fetches so a user click is not swallowed
during the first-scroll-to-bottom settle period.
Automatic top-sentinel loads still respect settling.
Fixes#1067
---------
Co-authored-by: Himehane <36065996+Himehane@users.noreply.github.com>
Co-authored-by: LIUZHIRU <ryuu@fine-net.co.jp>
* fix codex session import merge (#1123)
修复 Codex 会话导入合并后列表为空的问题。
Fix Codex session import merge so a session is not detected as a duplicate of itself and deleted during merge.
Co-authored-by: LIUZHIRU <ryuu@fine-net.co.jp>
* fix(web): keep overlays below PWA status bar
Account for env(safe-area-inset-top) on fixed top banners, image
preview chrome, and session action menus so installed PWA mode no
longer draws controls under the OS status bar.
Fixes#1066
---------
Co-authored-by: Himehane <36065996+Himehane@users.noreply.github.com>
Co-authored-by: LIUZHIRU <ryuu@fine-net.co.jp>