* feat(web): add LaTeX math formula rendering with KaTeX
Add remark-math + rehype-katex to the markdown rendering pipeline
so inline ($...$) and display ($$...$$) math formulas are rendered
as proper KaTeX output in chat messages and tool results.
Closes#237
* fix(web): disable single-dollar math parsing and add KaTeX to reasoning
- Set singleDollarTextMath: false to prevent $HOME, $PATH etc from
being misinterpreted as math formulas. Only $$...$$ (display) is
parsed; inline math requires explicit \(...\) or $$...$$.
- Add rehypePlugins to the reasoning renderer so math formulas
render consistently across chat, tool results, and reasoning blocks.
* refactor(web): use satisfies for type-safe plugin exports
Replace any[] with satisfies NonNullable<MarkdownTextPrimitiveProps[...]>
to preserve type safety on the shared plugin lists without needing
eslint suppressions.
* fix(web): enable single-dollar inline math syntax
Re-enable $...$ parsing (remark-math default) so inline formulas
like $E=mc^2$ render correctly. Shell variables like $HOME typically
appear inside code spans/blocks which remark-math does not parse,
so false positives are minimal in practice.
- Extend JWT expiration from 15 minutes to 4 hours in both auth and
bind endpoints. 15 minutes was too short — browser timer throttling
in background tabs prevented the scheduled refresh from firing
before expiration, causing unexpected logouts.
- Change the visibility/focus refresh from conditional (minTtlMs) to
forced, so returning to a backgrounded tab always re-authenticates
regardless of remaining token TTL. This eliminates the race between
timer throttling and token expiration.
HAPI is a self-hosted tool, so the longer token lifetime is an
acceptable security tradeoff. The auth source (Telegram initData or
CLI access token) is still validated on every refresh.
Closes#412
The process is spawned with `shell: process.platform === 'win32'` while including dynamic values (e.g., `opts.sessionId`) in `args`. On Windows, shell invocation can introduce command parsing/injection risks if arguments are not strictly validated/escaped.
Affected files: opencodeLocal.ts
Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
* fix(hub): allow terminal re-registration after socket reconnect
When a web client reconnects (common in PWAs and after network
hiccups), it retains the same terminal ID but gets a new socket ID.
The previous code rejected the registration because the old entry
still existed, producing "Terminal ID is already in use".
Now the registry treats a different-socket registration for the same
terminal ID as a stale entry and cleans it up before re-registering.
Same-socket re-registration returns the existing entry (idempotent).
Terminal IDs are client-generated UUIDs so cross-client collisions
are not a realistic concern.
Closes#345
* fix(hub): skip terminal quota check on reconnect
When a stale terminal entry still occupies a slot, the per-session
and per-socket quota checks reject the reconnecting client before
register() can clean up the stale entry. Detect reconnects (same
terminalId + sessionId already registered) and bypass quota checks
so the stale entry is properly replaced in register().
* fix(hub): reject cross-session terminal ID reuse
Only allow stale-entry replacement when the existing entry belongs
to the same session. If a different session happens to present the
same terminal ID, reject it as before to prevent one session from
evicting another session's active terminal.
The convenience `io()` function misparses the `/terminal` path
component as part of the Engine.IO endpoint in some browser
environments, producing requests to `/terminal/socket.io/` instead
of `/socket.io/`. Using `new Manager(baseUrl)` + `manager.socket('/terminal')`
separates the transport URL from the namespace unambiguously.
Closes#251
* fix(web): allow multiline input with modifier+Enter in composer
Previously only Shift+Enter was recognized for newline insertion while
Ctrl+Enter, Alt+Enter, and Cmd+Enter all triggered message send.
This broadens the modifier check so any modifier+Enter inserts a newline.
Also sets submitOnEnter={false} to let the custom handleKeyDown manage
all Enter logic, eliminating dual-handler ambiguity with the library's
built-in submit behavior.
Closes#429
* fix(web): prevent modifier+Enter from accidentally sending messages
Only plain Enter should send; Ctrl/Alt/Cmd+Enter were incorrectly
falling through to the send path because the guard only checked
e.shiftKey. Now all non-Shift modifier combos are blocked from
sending (preventDefault + no-op).
Also keeps submitOnEnter={false} so the custom handleKeyDown is the
sole owner of Enter-key logic, avoiding dual-handler ambiguity.
Closes#429
* fix(web): restore Enter to accept autocomplete suggestions
The previous refactor made the Enter handler unconditionally return
before reaching the suggestion-selection path. Move suggestion
handling above the send/no-op block so Enter still accepts visible
autocomplete entries.
* fix(cli,hub): resolve typecheck errors in codex reasoning effort and notification test
- Cast `getModelReasoningEffort()` return (string | null) to
`ReasoningEffort | undefined` at three call sites in
codexLocalLauncher.ts and runCodex.ts where the narrower type is
expected.
- Add missing `modelReasoningEffort: null` default in
notificationHub.test.ts to satisfy the Session type contract.
These errors were introduced in 79a13d2 and have been failing CI on
main since 2026-04-10.
* fix(cli): add missing getModelReasoningEffort to test mock session
The test stub in codexLocalLauncher.test.ts was missing the
getModelReasoningEffort method added in 79a13d2, causing runtime
TypeError in CI.
- Enter no longer inserts newline when sending is blocked (no-op instead)
- Shift+Enter now inserts newline (standard convention)
- Allow appending messages while agent is running (remove threadIsRunning
gate from canSend) — matches Claude Code CLI behavior where users can
queue messages during execution
The CLI wraps array-content user messages as agent output because
isExternalUserMessage rejects non-string content. On the web side,
detect text-only arrays in non-sidechain user output and emit them as
role:'user' so they display in the user lane.
Also handle sidechain user messages with mixed array content (e.g.
tool_result + text) by extracting text parts into a sidechain block.
Closes#407's original scope on top of the #402 base.
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
* fix(web): drop "No response requested." assistant messages
When Claude Code injects system messages (task notifications, system
reminders) as user turns, Claude responds with "No response requested."
In the HAPI web UI this appears as a reply to the user's message,
making it look like Claude is ignoring their input.
Filter these out in isSkippableAgentContent() (catches the fallback
path in normalize.ts) and in normalizeAssistantOutput() (catches the
primary path). Both checks verify the assistant message contains only
the text "No response requested." with no tool calls.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): filter text block instead of dropping entire message
Address bot review: dropping the whole normalized record breaks
sidechain UUID threading (parentUUID chain orphans).
Instead of returning null, suppress only the "No response requested."
text block during content extraction. The message record (uuid,
parentUUID, usage) is preserved so the tracer's sidechain grouping
continues to work.
Also remove the isSkippableAgentContent check since we no longer
need to drop the message at that layer.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): move "No response requested." filter to reducer layer
Address bot review: filtering in normalizeAssistantOutput() produced
empty content arrays, breaking traceMessages() which reads uuid and
parentUUID from content[0]. Sidechain child messages whose parentUUID
pointed to the filtered message became orphaned.
Fix: revert the normalizer to always emit the text block (preserving
the UUID chain for the tracer), and filter the sentinel text in
reducerTimeline.ts where text blocks become visible AgentTextBlocks.
At this point tracing is already complete.
Also adds reducer-level tests for the filter and updates the
normalize test to verify the text block is preserved.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): scope sentinel filter to single-block assistant messages only
Address bot review: the previous filter suppressed any text block
matching "No response requested.", which could hide legitimate replies.
Now the filter only triggers when the message has exactly one content
block (msg.content.length === 1) — i.e., the assistant response is
purely the sentinel text with no tool calls or reasoning blocks.
This prevents false positives while still catching the system-injection
auto-reply case.
Add test for the multi-block case (text + tool call) to verify the
sentinel text is preserved when other content exists.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): add parentUUID structural check to sentinel filter
Address bot review: raw text match alone could theoretically suppress
a legitimate reply. Add c.parentUUID !== null as a structural guard:
- Sentinel auto-replies always follow a prior assistant turn, so their
parentUUID is set (pointing to the previous message in the chain).
- A first message in a conversation has parentUUID: null and will
never be filtered.
Combined conditions: msg.content.length === 1 (sole block, no tool
calls) AND c.parentUUID !== null (not the first reply) AND exact text
match.
Add tests for the parentUUID=null escape hatch.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): use injected-turn UUID tracking for sentinel filter
Address review: use structural markers instead of broad text matching.
1. Pre-scan collects UUIDs from sidechain content blocks (system-
injected user turns). The sentinel filter now only triggers when
parentUUID points to one of these known injected turns.
2. Move task-notification event extraction from normalizer to reducer.
Previously, task-notifications with summary were normalized as
role:'event', losing their uuid. Now they stay as sidechain (uuid
preserved for pre-scan), and the reducer extracts the summary as
an agent-event block.
3. Remove redundant 'uuid' in c guard (always present on sidechain type).
False positive analysis: a legitimate reply is only suppressed when ALL
of: (a) sole content block, (b) parentUUID matches a sidechain-injected
turn, (c) exact sentinel text. This combination cannot occur for real
user-facing content.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): add parentUUID to sidechain content type for tracer linkage
The sidechain content block was missing parentUUID, so traceMessages()
could not chain system-injected user turns (task notifications, system
reminders) inside a Task sidechain back to their parent. This caused
later sidechain messages pointing to the injected turn's UUID to become
orphaned and disappear from the Task card.
Add parentUUID to the sidechain type definition and propagate it from
normalizeUserOutput() in both the isSidechain and non-sidechain paths.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): handle array-content sidechain user messages to prevent prompt leak
Sidechain user messages can arrive with either string content or array
content ([{type:'text', text:'...'}]) depending on how Claude Code
serialises them. The previous fix only handled the string case, causing
intermittent prompt leaks when array format was used.
Now normalizeUserOutput extracts text from array-content sidechain
messages and emits them as sidechain blocks, so the tracer can match
them to their parent Task tool call.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* test(web): verify parentUUID propagation from assistant output data
Add integration tests confirming normalizeAssistantOutput correctly
maps data.parentUuid to text block parentUUID (used by the reducer's
sentinel detection). Tests cover both present and absent parentUuid.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* fix(cli): filter raw SSE event JSON from leaking into chat messages
Two types of internal JSON were appearing as visible text in Telegram
Mini App and web chat:
1. `rate_limit_event` — the rate limit parser returned `null` for
unknown statuses, causing raw JSON to pass through as assistant text.
Changed to `{ suppress: true }` so all rate_limit_event variants are
handled; new statuses that need display can be added explicitly.
2. `{ type: "output", data: { ... } }` — internal session metadata
envelopes leaked through the ACP text chunk pipeline. Added an
`isInternalEventJson` filter that catches JSON objects with known
internal envelope types (output, event, queue-operation) before they
enter the text buffer.
Closes#386
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): narrow internal event filter to match only leaked metadata shape
Address review feedback: the broad type-based filter could suppress
legitimate assistant JSON with type "event" or "queue-operation".
Narrow the check to only match the specific leaked metadata envelope:
{ type: "output", data: { parentUuid, sessionId, userType } }
Add negative tests confirming other JSON types pass through.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): suppress malformed rate_limit_event without resetsAt
Address review: rate_limit_event payloads missing resetsAt still leaked
as raw JSON because parseRateLimitText returned null before reaching the
unknown-status suppress. Move the allowed check before the resetsAt
guard and suppress malformed payloads instead of passing them through.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): handle parentUuid: null in internal event filter
Root/first-message metadata envelopes have parentUuid: null rather than
a string, so the filter missed them. Accept both string and null.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* test(cli): add e2e regression tests for metadata envelope filtering
Add AcpMessageHandler tests that verify leaked { type: "output", data }
metadata envelopes (both parentUuid string and null) are dropped before
reaching the text buffer.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): clear buffered prefix when cumulative metadata chunk arrives
When a leaked metadata envelope arrives as cumulative streaming chunks
(first an incomplete JSON prefix, then the full blob), the filter
dropped the full chunk but left the prefix in bufferedText. Clear the
buffer when the detected internal JSON starts with the buffered prefix.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): round resetsAt to integer for pipe-delimited format
The web-side regex uses \d+ to parse the timestamp, so a float value
would silently fail to match. Apply Math.round to ensure integer output.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): clear buffered prefix for cumulative rate_limit_event chunks
The prefix-clearing logic only applied to the isInternalEventJson
branch but not to the parseRateLimitText branch, so cumulative
rate_limit_event chunks could leave a raw JSON prefix in the buffer.
Hoist the prefix check before both filters and apply uniformly.
Add regression tests for suppressed and displayable cumulative
rate_limit_event scenarios.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* fix(cli): continue execution after plan mode in YOLO/bypassPermissions
In YOLO mode (bypassPermissions), exit_plan_mode was auto-approved like
any other tool, skipping the PLAN_FAKE_RESTART injection that tells the
agent to continue. Combined with isAborted() always returning true for
exit_plan_mode, claudeRemote exited the query loop and stalled waiting
for user input.
Fix: in the bypassPermissions branch of handleToolCall, intercept
exit_plan_mode specifically — inject PLAN_FAKE_RESTART into the message
queue and return deny with PLAN_FAKE_REJECT, matching the behavior of
the normal approval flow.
Closes#172
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* test(cli): remove unused isPlanTool helper
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
The copy button was absolutely positioned (right-0 top-0) over the
entire message content area. When assistant messages contain both text
and tool calls (e.g. TodoWrite), the button overlapped the tool card UI.
Move the button from absolute positioning inside the content wrapper to
an inline flex layout after the content. This places it at the bottom-
right of the message, below all content (text + tool cards), so it never
overlaps anything. The hover-to-reveal behavior is preserved.
Also restores getAssistantCopyText to its original logic so mixed
text+tool messages remain copyable (the previous fix of suppressing the
button entirely for mixed messages was too aggressive).
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
The release workflow produces `hapi-linux-x64-baseline.tar.gz` (from
bun-linux-x64-baseline target), but the Homebrew formula generator wrote
the URL as `hapi-linux-x64.tar.gz`, causing `brew install` to fail on
Linux x64 with a 404.
Closes#365
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
* fix(hub): raise maxRequestBodySize so file uploads work
The Bun server inherited maxRequestBodySize from Socket.IO's default
maxHttpBufferSize (1 MB). The upload endpoint sends files as base64
in JSON, so any image > ~750 KB was silently rejected before reaching
the route handler. The frontend allows 50 MB uploads.
Raise the limit to at least 100 MB to accommodate 50 MB files with
base64 encoding overhead (~33%).
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(hub,web): fix file uploads — raise body limit, lower max size, show errors
Three changes:
1. hub/server.ts: Bun's maxRequestBodySize inherited Socket.IO's 1 MB
default, silently rejecting any upload. Raise to 10 MB.
2. hub/routes + web/attachmentAdapter: lower MAX_UPLOAD_BYTES from
50 MB to 5 MB (realistic for images; 5 MB base64 ≈ 6.7 MB body,
fits within the 10 MB server limit).
3. web/AttachmentItem: show "Upload failed" text and strike-through
filename on error, instead of just a tiny icon.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(hub): keep 50MB upload limit, size maxRequestBodySize to match
Bot review correctly flagged that lowering MAX_UPLOAD_BYTES to 5 MB
regresses the documented 50 MB limit. Revert to 50 MB and calculate
maxRequestBodySize properly: 50 MB × 4/3 (base64) + 1 MB (JSON
overhead) ≈ 68 MB.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* fix(web): reconnect SSE immediately when tab becomes visible
The SSE watchdog skips heartbeat checks while the tab is hidden. If the
connection dies in the background, the user sees stale messages after
switching back and has to wait up to 10 s for the next watchdog tick.
Add a visibilitychange listener that checks heartbeat staleness
immediately when the tab becomes visible and reconnects if stale.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): map visibility-recovery reason in reconnecting banner
The new 'visibility-recovery' reconnect reason was not mapped in
getReasonLabel(), so the raw string would appear in the UI banner.
Add localized labels for both en and zh-CN.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* fix(web): disable indented code blocks in markdown rendering
In CommonMark, text indented by 4+ spaces is treated as a code block.
LLM responses frequently have indented content inside numbered lists or
quoted text, causing large chunks to render as a single code block
instead of formatted markdown.
Add a remark plugin that disables the codeIndented tokenizer. Fenced
code blocks (``` … ```) continue to work normally.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): fix typecheck for remark plugin this binding
Use `as any` cast for the unified processor `this` context instead of
an explicit type annotation that conflicts with the Processor type.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): use this:unknown annotation to satisfy noImplicitThis
The previous `as any` cast on `this` still triggers noImplicitThis in
strict mode. Annotate the parameter as `this: unknown` and cast to
the required shape inside the function body. Also use the key-based
`data(key, value)` API instead of mutating the returned object.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
The rename endpoint uses PATCH /api/sessions/:id, but the CORS
middleware only allowed GET, POST, DELETE, OPTIONS. Browsers send a
preflight OPTIONS request for PATCH; without it in allowMethods the
preflight fails and the request never reaches the handler, causing
"Failed to rename" in the web UI every time.
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
* refactor(web): extract normalizeTimestamp helper in presentation
Extract the shared seconds-vs-milliseconds normalization logic into
a private `normalizeTimestamp()` helper. No behavior change —
`formatUnixTimestamp()` produces identical output.
* refactor(web): return AgentEvent from parseClaudeUsageLimit
Change return type from `number | null` to `AgentEvent | null` so
the caller doesn't need to construct the event object. No behavior
change — the same `limit-reached` event is produced.
* feat(cli): convert rate_limit_event to standardized text format
Parse undocumented Claude `rate_limit_event` JSON in the CLI adapter
layer (AcpMessageHandler) before it reaches the web.
Converted text format (pipe-delimited):
- "Claude AI usage limit warning|{ts}|{pct}|{rateLimitType}"
- "Claude AI usage limit reached|{ts}|{rateLimitType}"
Status handling:
- `allowed_warning` → warning text with utilization and limit type
- `rejected` → reached text with limit type
- `allowed` → silently suppressed (noise)
- unknown statuses → passed through as-is (forward-compatible)
* feat(web): display rate limit warnings with limit type
Parse standardized pipe-delimited text from the CLI adapter into
`limit-warning` and `limit-reached` events, displaying the rate
limit type (5-hour, 7-day) when available.
- `limit-warning`: "⚠️ Usage limit 90% (5-hour) · resets 2:00 PM"
- `limit-reached`: "⏳ Usage limit reached (5-hour) until 4/2/2026"
- Backward compatible: `limit-reached` without limitType still works
The `reached` regex uses `(?:\|([^|]*))?$` to optionally match the
limitType field, maintaining compatibility with the existing format.
* refactor(cli): move rate limit parsing out of flushText
Remove rate limit detection from flushText() back to plain buffer
flush. The next commit will re-add parsing at the chunk level
(handleUpdate) where it can intercept before buffer merging.
Includes failing tests that demonstrate the mixed-chunk bug:
when a rate_limit_event chunk arrives in the same turn as normal
text, the JSON leaks into the merged buffer.
* fix(cli): intercept rate_limit_event at chunk level, not flush
Move rate limit detection from flushText() to the agentMessageChunk
handler so it fires before the chunk enters the shared text buffer.
Previously, a rate_limit_event chunk arriving in the same turn as
normal text would merge into bufferedText and leak as raw JSON.
Now the chunk is intercepted individually, the existing buffer is
flushed first (preserving prior text), and the converted message
is emitted separately.
* fix(cli): skip flush when suppressing allowed rate_limit_event
Only flush the text buffer when the parsed event will actually be
displayed. Suppressed events (e.g. status: 'allowed') now return
immediately without flushing, preventing a text → allowed → text
sequence from splitting one answer into two agent-text blocks.
* fix(web): include limitType in limit-reached reconcile key
Without this, reprocessing a message from the old format (no
limitType) to the new typed format reuses the stale block and
the (5-hour)/(7-day) suffix never appears.
* fix(web): filter system-injected XML tags from rendering as raw text
Claude Code injects internal messages (<task-notification>, <system-reminder>,
<command-name>, <local-command-caveat>) as user-role messages. The web UI was
rendering these as raw XML text visible to users.
- Parse <task-notification> and display as agent-event with summary text
- Silently drop <system-reminder>, <command-name>, <local-command-caveat>
- Add tests covering all injection prefixes and edge cases
* fix(web): scope system injection filtering to Claude sessions only
Address review feedback: the XML tag filtering was applied at the
generic timeline layer, which could incorrectly hide legitimate user
messages in Codex/Gemini sessions.
- Add isClaudeSession flag threaded from Session.metadata.claudeSessionId
- Only filter system-injected tags when isClaudeSession is true
- Add tests verifying non-Claude sessions pass through all messages
* fix(web): treat all string user output as sidechain to prevent prompt leaks
Restores the fix from 3cf96ab that was accidentally reverted in 2205e04.
In normalizeUserOutput(), string-content user messages arriving through
the agent output path are never real user input (real user text goes
through normalizeUserRecord). Previously, non-sidechain string messages
were emitted as role:'user', causing subagent prompts and system-injected
messages to render as user text in the web UI.
Now all string-content user messages in this path are:
- <task-notification> with summary → converted to role:'event'
- Everything else → marked as sidechain (matched to parent Task tool
call by the tracer, or harmlessly skipped by the reducer)
This provides a root-level fix that prevents ANY string user message
from the agent output path from leaking as visible user text.
* ci: retrigger CI
* fix(web): remove superseded return-null filter from upstream PR #372
The upstream `return null` filter for <task-notification> and
<system-reminder> (from PR #372) is now superseded by the comprehensive
sidechain upgrade logic. Remove it to avoid short-circuiting the new
task-notification → event conversion.
* refactor(web): remove reducer-side system injection filtering
System-injected messages are now fully handled in normalizeUserOutput()
(normalize layer), so the redundant filtering in reduceTimeline() is no
longer needed. Removing it also eliminates the risk of accidentally
hiding legitimate user messages that happen to start with XML tags.
- Remove SYSTEM_INJECTION_PREFIXES, isSystemInjectedMessage,
parseTaskNotificationSummary from reducerTimeline.ts
- Remove isClaudeSession plumbing from reducer.ts and SessionChat.tsx
- Simplify reducerTimeline.test.ts to only test pass-through behavior