The SSE reader split events only on "\n\n", so providers or proxies
emitting CRLF or bare-CR line terminators broke event framing. It also
let SseReader.buf grow without bound when a stream never produced an
event boundary. Streaming requests had no retry on the initial send, and
the Responses-API tool-call accumulator used a HashMap, yielding
non-deterministic tool-call order.
- Normalize CR and CRLF to LF on chunk append (SSE spec equivalence)
- Cap SseReader.buf at MAX_LLM_RESPONSE_BYTES, erroring before any
unbounded growth
- Add send_stream_with_retry() applying the buffered path's transport/
5xx/429 backoff to the initial request only, stopping before any chunk
is emitted to avoid duplicate output
- Replace the HashMap tool-call accumulator with a first-seen-ordered Vec
to match the buffered parser's ordering
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
output_item.added may arrive after function_call_arguments.delta when
the Responses API reorders events. The delta handler creates the HashMap
entry with an empty name, and the previous or_insert_with no-oped on
existing entries. Use and_modify to backfill the name regardless of
insertion order.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Replace buffered LLM requests with Server-Sent Events streaming. Each
text delta emits an agent_message_chunk session update as it arrives,
providing a natural keepalive that resets the ACP idle clock without
relying solely on the 30s ticker.
Design decisions:
- Two reqwest::Client instances: `http` (with global timeout for
summarize) and `http_stream` (no global timeout, enforces first-byte
and inter-chunk timeouts via tokio::time::timeout)
- Two-phase timeout: llm_timeout (120s) until first content delta,
then stream_chunk_timeout (30s) between subsequent events
- Anthropic: index-keyed HashMap<usize, (String, String)> for parallel
tool-call accumulation via content_block index
- OpenAI Chat: Vec-indexed accumulation by tool_calls[].index
- Responses API: routes on JSON `type` field inside data payload, uses
response.output_text.delta and response.function_call_arguments.delta
- MAX_LLM_RESPONSE_BYTES caps accumulated semantic content (text + tool
args), not raw SSE wire bytes
- SSE parser handles : comments, multi-line data:, id:/retry: fields
- Keepalive ticker (G) preserved as fallback for reasoning models that
pause before producing content
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Death notices referenced the thread root with NIP-10 marker "reply"
instead of "root", causing incorrect threading in clients. Tag-parse
errors incorrectly mapped to RelayError::AuthFailed.
- Change e-tag marker from "reply" to "root" in build_death_notice
- Add RelayError::EventBuild variant for tag-parse failures
- Add publish_death_notice() method on HarnessRelay to consolidate
the build+publish+warn pattern used by both timeout and transport
error paths
- Replace inline match blocks in handle_prompt_result with method call
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Address review findings from Thufir on PR #935:
- Post death notice on transport-error respawns (Io, WriteTimeout,
Timeout, Protocol) — same user-facing silence as idle timeout
- Thread death notices into the original conversation via e-tag reply
so users know which task died in busy channels
- Add explicit "keepalive" match arm in handle_session_update for
documentation clarity
- Update idle-reset comment to clarify belt-and-suspenders intent
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The 20s margin between the 600s max shell timeout and the 620s idle
timeout caused spurious session kills during legitimate long-running
tool calls. This changeset addresses the problem from four angles:
- Raise DEFAULT_IDLE_TIMEOUT_SECS from 620 to 900 (300s margin)
- Reset idle clock explicitly on tool_call session updates with
observability logging
- Post a visible channel message (death notice) when a session ends
due to idle timeout or unexpected agent exit
- Emit a keepalive session update every 30s while waiting on the LLM
provider response, preventing idle timeout during slow completions
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>