Trigger-fired visibility, stream-stall guards, and console reliability (MOT-3949…MOT-3954) - #468
Conversation
…nance Every real fire now appends a model-invisible `trigger_fired` custom entry to the owner session (subscriptions/fired.rs): notify fires, react simple spawns, join arrivals, and join completions — with entry ids idempotent against engine redelivery. One durable artifact drives both the console's chat fire notices and the panel's fired-trigger ghosts. Child sessions are stamped `spawned_by: trigger|agent` and direct spawn seed entries get `e_spawn_` ids + a spawn origin, so the console can tell trigger-fired work from agent-spawned work.
…ing args tail, agent_trigger no-target error - clients/router.rs: the frame loop races the held-open router::chat trigger (750ms grace drain) so a dispatch failure before a writer attaches fails the turn instead of hanging it forever; synthesized error kind Permanent/Transient by response error. - clients/router.rs: accumulate raw FunctioncallDelta text and ride a bounded `_streaming` tail on coalesced partials while a call's arguments are still forming — UIs can show the command being written; disappears once args parse. - turn_loop/trigger.rs: a call still targeting `agent_trigger` at dispatch (arguments empty/null/unparseable) fails locally with a teachable error instead of a doomed engine dispatch (`function_not_found: agent_trigger`); wrapper sentinel pinned in policy.rs.
…y lifecycle, id-only models::get, ping/idle guard, args salvage
- registry/store.rs: restore resets every record to available:false
(pessimistic — availability is proven by a send/refresh, not history);
chat.rs heals it back on RelayResult::Done + emits provider::changed;
register.rs boot-nudges every known provider's on_router_ready after
READY (the engine drops router::ready bindings when the router
disconnects, so re-declaration can't depend on them). (MOT-3950)
- catalog: models::get treats an empty wire provider as unset and resolves
id-only lookups when the id is unambiguous across slices — react turns
(no provider on the spec) no longer fall to the 8k fallback window and
compact every step. (MOT-3951)
- chat/relay.rs: once content has started, only non-ping frames reset the
idle budget — a provider pinging past a dead upstream trips Idle at
idle_timeout_ms instead of zombie-ing to the engine's 300s stream_timeout
("stream ended without a terminal frame"). Pre-content pings still reset
(slow first token behind keepalives stays legitimate). (MOT-3952)
- types/messages.rs: degraded_arguments — shared salvage for unparseable
tool-call args: recover the complete leading fields of a partial object,
else carry the malformed text as {"_raw": …}; always an object
(replay-safe), evidence preserved. (MOT-3953)
… + salvage degraded tool args
- read_timeout(120s) on the upstream reqwest client in the five cloud
providers: a stalled connection otherwise pings the router past its idle
guard until the engine kills the call at stream_timeout. Bounds silence
between reads, not stream length (healthy streams emit SSE pings).
llamacpp deliberately skipped — no keepalives and legitimately slow
prompt eval. (MOT-3952)
- all six providers degrade unparseable tool-call arguments through the
shared llm_router degraded_arguments: mid-stream partials keep their
known leading fields (a long-streaming call stays identifiable as
ƒ state::set instead of an anonymous {}), malformed finals keep the raw
text as {"_raw": …} evidence — always an object, so cross-provider
replay can't 400 the way null/bare-string did. (MOT-3953)
… visibility, error surfacing, streaming args pane, re-hydration
- Fired triggers stay visible: trigger_fired chat notices (no new turn),
panel ghosts "fired · unregistered" with dismiss + counts, full-row
retention so the workflow DAG survives retirement; spawn seed tasks
render like reaction tasks; sidebar Zap/Bot provenance icons from
spawned_by. (MOT-3949)
- Error surfacing: session::messages read-backs deliver custom entries as
role:"custom" — the mapper handles both wire shapes via one dispatcher,
so error/turn-failed/compaction/trigger-fired entries actually render;
stream-rescue aborts the spinner when the session errors; the waiting
shimmer shows the under-the-hood detail; model picker disables models of
unavailable providers ("not loaded") and refreshes on
router::provider::changed. (MOT-3950)
- Live tool args: unwrap the harness `_streaming` tail and render a
"request · streaming…" pane in FunctionCallCard while a call's arguments
form. (MOT-3953)
- markBackgroundedStale: backgrounded sessions re-hydrate on activation, so
entries frozen mid-snapshot during the away-gap self-repair from durable
truth. (MOT-3954)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
skill-check — worker0 verified, 41 skipped (no docs/).
Four for four. Nicely done. |
📝 WalkthroughWalkthroughThis PR adds durable trigger-fired records, spawn provenance, streaming lifecycle safeguards, provider availability events, model catalog updates, stream timeout controls, and degraded JSON argument preservation across the harness, router, provider adapters, and console. ChangesTrigger lifecycle and spawn provenance
Streaming lifecycle and partial tool calls
Provider availability and catalog updates
Stream timeouts and degraded arguments
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant TriggerHandler
participant OwnerSession
participant EntryMapper
participant ChatView
participant SessionTriggers
TriggerHandler->>OwnerSession: append trigger_fired record
OwnerSession->>EntryMapper: map typed transcript entry
EntryMapper->>ChatView: provide trigger-fired message
ChatView->>SessionTriggers: merge polled rows with fired history
SessionTriggers-->>ChatView: render live and dismissible fired rows
sequenceDiagram
participant Provider
participant RouterRelay
participant RouterRegistry
participant CatalogSubscription
participant ModelPicker
Provider->>RouterRelay: stream frames
RouterRelay->>RouterRegistry: mark provider available after completion
RouterRegistry->>CatalogSubscription: emit provider changed event
CatalogSubscription->>ModelPicker: reload provider catalog
ModelPicker-->>ModelPicker: disable unavailable provider models
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
console/web/src/components/chat/ChatView.tsx (1)
239-261: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winStale
listTriggersresponse can pollute the wrong conversation's trigger cache/panel.
refreshTriggershas no guard against out-of-order async responses across conversation switches. If a request for conversation A resolves after the user switches to conversation B, its rows get written into the freshly-resetseenTriggersRef.currentand intosetSessionTriggers, showing A's triggers (and permanently retaining A's fired-ghost rows) under B. The siblinglistQueuedeffect a few lines above already guards this exact class of race with analiveflag — this new code doesn't follow that same pattern.🔒 Proposed fix using the same staleness-guard pattern as the file's own `listQueued` effect
+ const activeConversationRef = useRef(conversation.id) + activeConversationRef.current = conversation.id + const refreshTriggers = useCallback(() => { const listTriggers = backend.listTriggers if (!listTriggers) return - listTriggers(conversation.id) + const requestedFor = conversation.id + listTriggers(requestedFor) .then((rows) => { + if (activeConversationRef.current !== requestedFor) return for (const row of rows) seenTriggersRef.current.set(row.id, row) setSessionTriggers(rows) }) .catch(() => {}) }, [backend.listTriggers, conversation.id])🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@console/web/src/components/chat/ChatView.tsx` around lines 239 - 261, Guard asynchronous listTriggers responses against stale conversation effects, matching the alive-flag pattern used by the listQueued effect. Update refreshTriggers and the polling useEffect so responses from a prior conversation cannot modify seenTriggersRef or setSessionTriggers after cleanup; ensure the interval and callback use the same effect-local liveness state.
🧹 Nitpick comments (1)
llm-router/src/register.rs (1)
221-246: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider concurrent provider nudges for faster availability recovery.
The sequential
for id in ids { ... .await }loop means a dead provider early in the list (10s timeout) delays the nudge to every subsequent live provider. If K of N providers are dead, the last live provider waits up to K×10s. Movingtokio::spawninside the loop triggers all providers concurrently, capping total time at 10s regardless of count.♻️ Proposed refactor: concurrent per-provider triggers
{ let iii = iii.clone(); let ids = registry.ids().await; - tokio::spawn(async move { - for id in ids { - let _ = iii - .trigger(TriggerRequest { - function_id: format!("provider::{id}::on_router_ready"), - payload: json!({}), - action: None, - timeout_ms: Some(10_000), - }) - .await; - } - }); + for id in ids { + let iii = iii.clone(); + tokio::spawn(async move { + let _ = iii + .trigger(TriggerRequest { + function_id: format!("provider::{id}::on_router_ready"), + payload: json!({}), + action: None, + timeout_ms: Some(10_000), + }) + .await; + }); + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@llm-router/src/register.rs` around lines 221 - 246, Make the provider readiness nudges concurrent instead of awaiting each trigger sequentially. In the spawned block after collecting registry IDs, have the loop spawn a separate task for each provider’s `iii.trigger` call using `provider::{id}::on_router_ready`, allowing dead-provider timeouts to occur independently and live providers to recover without waiting.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@harness/src/clients/router.rs`:
- Around line 269-276: Update the frame-less acknowledged-stream handling near
the error-kind assignment so the synthesized transient error message is stored
in the outcome’s error field instead of returning error: None. Adjust the
success predicate in the surrounding response construction to require both
acknowledgment success and no stop reason, using the synthesized message’s stop
reason.
In `@harness/src/trigger.rs`:
- Around line 142-144: In wrapper_without_target_result, replace the
byte-indexed String::truncate(200) with char-safe truncation that limits the
serialized arguments to 200 characters without splitting UTF-8 sequences,
preserving the existing preview behavior and avoiding panics for non-ASCII model
output.
In `@provider-llamacpp/src/sse.rs`:
- Around line 98-102: Update the argument parsing in the SSE function-call
handling to accept decoded JSON only when the resulting serde_json::Value is an
object; otherwise, fall back to
llm_router::types::messages::degraded_arguments(&fc.args_json), matching the
Anthropic path’s validation behavior.
In `@provider-openai-codex/src/sse.rs`:
- Around line 101-105: Update the tool-argument parsing in the SSE handling code
to accept only JSON objects: parse with serde_json, verify the result is an
object, and otherwise use
llm_router::types::messages::degraded_arguments(&tc.args_json), matching the
Anthropic path and preventing scalars or arrays from reaching
ContentBlock::FunctionCall.arguments.
In `@provider-openai/src/sse.rs`:
- Around line 87-91: In the function-call argument parsing logic, ensure parsed
JSON is accepted only when it is an object: update the serde_json fallback
around fc.args_json to reject null, strings, arrays, and booleans by checking
is_object(), then call llm_router::types::messages::degraded_arguments() for all
non-object or malformed values.
In `@provider-xai/src/sse.rs`:
- Around line 97-101: In the argument parsing expression within the SSE
function-call handling, ensure only parsed JSON objects are accepted: parse into
a serde_json::Value, retain it only when Value::is_object() returns true, and
otherwise call llm_router::types::messages::degraded_arguments(&fc.args_json) so
scalars and arrays cannot reach FunctionCall.arguments.
---
Outside diff comments:
In `@console/web/src/components/chat/ChatView.tsx`:
- Around line 239-261: Guard asynchronous listTriggers responses against stale
conversation effects, matching the alive-flag pattern used by the listQueued
effect. Update refreshTriggers and the polling useEffect so responses from a
prior conversation cannot modify seenTriggersRef or setSessionTriggers after
cleanup; ensure the interval and callback use the same effect-local liveness
state.
---
Nitpick comments:
In `@llm-router/src/register.rs`:
- Around line 221-246: Make the provider readiness nudges concurrent instead of
awaiting each trigger sequentially. In the spawned block after collecting
registry IDs, have the loop spawn a separate task for each provider’s
`iii.trigger` call using `provider::{id}::on_router_ready`, allowing
dead-provider timeouts to occur independently and live providers to recover
without waiting.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 19395e52-485e-4bc3-ba39-610ce6a101b7
📒 Files selected for processing (45)
console/web/src/components/chat/ChatView.tsxconsole/web/src/components/chat/Message.tsxconsole/web/src/components/chat/MessageList.tsxconsole/web/src/components/chat/ModelPicker.tsxconsole/web/src/components/chat/SessionTriggers.tsxconsole/web/src/components/function-call/FunctionCallCard.tsxconsole/web/src/components/sidebar/ConversationRow.tsxconsole/web/src/hooks/use-conversations.test.tsconsole/web/src/hooks/use-conversations.tsconsole/web/src/hooks/use-model-picker-source.tsconsole/web/src/lib/backend/triggers.test.tsconsole/web/src/lib/backend/triggers.tsconsole/web/src/lib/models-catalog.tsconsole/web/src/lib/sessions/entry-mapper.test.tsconsole/web/src/lib/sessions/entry-mapper.tsconsole/web/src/types/chat.tsharness/src/clients/router.rsharness/src/functions/react.rsharness/src/ids.rsharness/src/policy.rsharness/src/subagent.rsharness/src/subscriptions/fired.rsharness/src/subscriptions/mod.rsharness/src/subscriptions/notify_agent.rsharness/src/trigger.rsharness/src/turn_loop.rsllm-router/src/catalog/handlers.rsllm-router/src/catalog/queries.rsllm-router/src/chat/chat.rsllm-router/src/chat/relay.rsllm-router/src/register.rsllm-router/src/registry/store.rsllm-router/src/types/messages.rsllm-router/tests/integration.rsprovider-anthropic/src/register.rsprovider-anthropic/src/sse.rsprovider-llamacpp/src/sse.rsprovider-openai-codex/src/register.rsprovider-openai-codex/src/sse.rsprovider-openai/src/register.rsprovider-openai/src/sse.rsprovider-xai/src/register.rsprovider-xai/src/sse.rsprovider-zai/src/register.rsprovider-zai/src/sse.rs
Without the fine-grained-tool-streaming-2025-05-14 beta, the Messages API buffers tool_use input server-side: measured one ~108B leading delta, then 119.4s of ping-only silence, then all 3434 deltas (376KB) in a 0.6s burst for a ~4k-token input. Large state::set-style args therefore exceed the router's 120s post-content idle budget and a healthy stream gets killed mid-args (the "stream idle past 120000ms" failures). With the beta header the same probe streams continuously (max 3.6s event gap), the idle guard only fires on real stalls, and the console's streaming-args pane gets live deltas. The beta's early-stop partial-JSON risk is already handled by degraded_arguments salvage.
…MOT-3953) Salvaged partial arguments are now stamped "_partial": true, and the turn loop refuses to dispatch any call whose arguments carry _partial/_raw, failing locally with a teachable arguments_truncated result instead. With fine-grained tool streaming a max_tokens cutoff mid-args ends the stream successfully, so the salvaged prefix would otherwise have executed with whatever fields happened to complete. Also closes the scalar bypass in five providers (openai, xai, zai, llamacpp, openai-codex): parseable non-object args (null, strings, arrays) now degrade like unparseable ones — anthropic already had the Value::is_object filter — keeping the object-only replay contract. And the teachable-error previews truncate on char boundaries; the byte-indexed String::truncate panicked on CJK/emoji model output. Found by codex review and coderabbit on PR #468.
…e guards (MOT-3949, MOT-3952, MOT-3954) - react: fired records use a per-fire entry id (spawned turn id) so recurring owner-delivered reactions stop deduping every fire after the first into one notice; the engine trigger id is resolved while the binding is live, teardown runs first, and retired reflects the actual unregister outcome — a failed teardown no longer renders a live, still-firing row as unregistered with only a local dismiss. - router client: after the ack, a 750ms lull only means EOF for failed dispatches; an ok response keeps draining for its terminal frame (10s cap) instead of synthesizing a no-terminal error over a healthy stream. A frame-less-but-acked stream now fails the outcome instead of completing ok around an empty error-stopped message. - console: transcript hydration is keyed on the active conversation's hydrated flag, so live entry events no longer cancel and restart the paginated fetch mid-stream; the thinking-level buttons are gated on provider availability, closing the side door that selected an unavailable model past its disabled row. Found by codex review and coderabbit on PR #468.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
console/web/src/hooks/use-conversations.ts (1)
393-396: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider extracting the duplicated
spawned_bymapping.The
md.spawned_by === 'trigger' || md.spawned_by === 'agent' ? md.spawned_by : ...mapping is duplicated here and inconversationFromMeta(Lines 146-149). A small helper (e.g.parseSpawnedBy(md.spawned_by)) keeps both call sites in sync if the allowed values ever change.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@console/web/src/hooks/use-conversations.ts` around lines 393 - 396, Extract the duplicated spawned_by validation into a shared helper such as parseSpawnedBy, accepting the metadata value and returning it only for 'trigger' or 'agent', otherwise returning the appropriate fallback. Replace the inline ternary in the current conversation mapping and the equivalent logic in conversationFromMeta so both call sites use the helper consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@provider-anthropic/src/request.rs`:
- Around line 16-25: Remove the obsolete ANTHROPIC_BETA constant and any usage
that adds fine-grained-tool-streaming-2025-05-14 to Anthropic requests. Enable
the equivalent behavior through the request’s eager_input_streaming
configuration instead, updating the relevant request-building logic while
preserving incremental tool input streaming.
---
Nitpick comments:
In `@console/web/src/hooks/use-conversations.ts`:
- Around line 393-396: Extract the duplicated spawned_by validation into a
shared helper such as parseSpawnedBy, accepting the metadata value and returning
it only for 'trigger' or 'agent', otherwise returning the appropriate fallback.
Replace the inline ternary in the current conversation mapping and the
equivalent logic in conversationFromMeta so both call sites use the helper
consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: dc51b21f-ab5d-4707-941c-112051bda216
📒 Files selected for processing (13)
console/web/src/components/chat/ModelPicker.tsxconsole/web/src/hooks/use-conversations.tsharness/src/clients/router.rsharness/src/functions/react.rsharness/src/trigger.rsharness/src/turn_loop.rsllm-router/src/types/messages.rsprovider-anthropic/src/request.rsprovider-llamacpp/src/sse.rsprovider-openai-codex/src/sse.rsprovider-openai/src/sse.rsprovider-xai/src/sse.rsprovider-zai/src/sse.rs
🚧 Files skipped from review as they are similar to previous changes (9)
- provider-zai/src/sse.rs
- provider-xai/src/sse.rs
- provider-openai-codex/src/sse.rs
- llm-router/src/types/messages.rs
- provider-openai/src/sse.rs
- provider-llamacpp/src/sse.rs
- console/web/src/components/chat/ModelPicker.tsx
- harness/src/turn_loop.rs
- harness/src/clients/router.rs
…header (MOT-3952) fine-grained-tool-streaming-2025-05-14 went GA as the per-tool eager_input_streaming flag, and stale anthropic-beta values get rejected by some gateways (e.g. Bedrock). Probe-verified on api.anthropic.com: with the flag and no header a ~1k-word tool input streams with a max 0.4s non-ping gap; the same request without it buffers server-side (40s ping-only silence, then one 134KB burst) — the signature that was tripping the router's 120s idle guard on large state::set args. Found by coderabbit on PR #468.
Stable clippy 1.97 added useless_borrows_in_formatting, failing the console rust lint CI job on a pre-existing test assertion.
…nest retirement, hydration replay (MOT-3952, MOT-3954)
Codex round-3 findings:
- plan_calls propagates _partial/_raw from the agent_trigger wrapper into
the unwrapped payload: a max_tokens cut landing after a complete payload
salvaged to {function, payload, _partial} and the unwrap shed the marker,
bypassing the turn loop's refusal to execute provider-degraded arguments.
- unregister_engine_trigger returns the real outcome; notify once-fires and
join completions record retired only when teardown actually succeeded,
instead of a dismiss-only console ghost for a trigger that can still fire.
- Provider upstream read_timeout (120s) is overridable via
PROVIDER_READ_TIMEOUT_SECS so a fixed cap can't undercut router
idle/stream budgets raised for slow self-hosted endpoints.
- Console hydration buffers live upserts that land while the transcript
fetch is in flight and replays them over the snapshot
(mergeHydratedTranscript), so the older read can't clobber a newer
revision that hydrated: true would then pin stale.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
provider-anthropic/src/register.rs (1)
109-128: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider extracting
read_timeout()into a shared utility.The
read_timeout()helper is identical across at least five provider crates (anthropic, openai, openai-codex, xai, zai). If these crates share a common workspace dependency or a small shared helper crate, extracting this function would prevent drift — e.g., one crate silently diverging on the default or env-var name. If cross-crate sharing isn't practical in this workspace, the duplication is acceptable given the function's small size.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@provider-anthropic/src/register.rs` around lines 109 - 128, Extract the duplicated read_timeout() logic used by the anthropic, openai, openai-codex, xai, and zai providers into an appropriate shared workspace utility, preserving the PROVIDER_READ_TIMEOUT_SECS environment variable and 120-second default, then update each provider’s register_provider implementation to use it. If no practical shared dependency exists, leave the local helpers unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@provider-anthropic/src/register.rs`:
- Around line 109-128: Extract the duplicated read_timeout() logic used by the
anthropic, openai, openai-codex, xai, and zai providers into an appropriate
shared workspace utility, preserving the PROVIDER_READ_TIMEOUT_SECS environment
variable and 120-second default, then update each provider’s register_provider
implementation to use it. If no practical shared dependency exists, leave the
local helpers unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a98444fa-bf24-4ba8-9c57-b92306696ca1
📒 Files selected for processing (14)
console/tests/integration.rsconsole/web/src/hooks/use-conversations.test.tsconsole/web/src/hooks/use-conversations.tsharness/src/functions/react.rsharness/src/functions/subscribe.rsharness/src/policy.rsharness/src/subscriptions/notify_agent.rsprovider-anthropic/src/register.rsprovider-anthropic/src/request.rsprovider-anthropic/src/wire/tools.rsprovider-openai-codex/src/register.rsprovider-openai/src/register.rsprovider-xai/src/register.rsprovider-zai/src/register.rs
✅ Files skipped from review due to trivial changes (2)
- console/tests/integration.rs
- provider-anthropic/src/request.rs
🚧 Files skipped from review as they are similar to previous changes (5)
- provider-zai/src/register.rs
- provider-xai/src/register.rs
- harness/src/subscriptions/notify_agent.rs
- console/web/src/hooks/use-conversations.test.ts
- harness/src/functions/react.rs
Five commits spanning six tickets — one feature and a set of reliability fixes that were all root-caused live on the multi-agent pipeline rig.
MOT-3949 — fired triggers stay visible
The trigger panel was a pure projection of currently-registered triggers: a
oncetrigger fired, the harness unregistered it, and the row silently vanished; fires produced nothing in the wiring chat. Now every real fire appends a durable, model-invisibletrigger_firedcustom entry to the owner session (notify, react spawns, join arrivals/completions, redelivery-idempotent entry ids). The console renders it as a ⚡ chat notice without starting a turn and keeps firedoncetriggers in the panel as dismissable "fired · unregistered" ghosts (workflow DAG survives retirement). Child sessions carryspawned_by: trigger|agent→ Zap/Bot sidebar provenance icons; direct spawn seed tasks render like reaction tasks instead of a typed user message.MOT-3950 — provider availability lifecycle + errors surface in chat
Router restarts restored dead providers as
available: true; after fixing that pessimistically, re-declaration deadlocked because the engine dropsrouter::readybindings when the router disconnects. Now: restore resets tofalse, a successful chat heals it back (+provider::changed), and the router boot-nudges each known provider'son_router_readydirectly. Console disables models of unavailable providers ("not loaded") and refreshes on provider changes. Chat error surfacing:session::messagesread-backs deliver custom entries asrole:"custom"— the mapper now handles both wire shapes, so turn-failed/error entries actually render, the spinner rescues on error status, and the waiting shimmer shows the under-the-hood detail.MOT-3951 — id-only
models::get(compaction every step)React turns carry no provider;
models::getwith an empty provider returnednull, so context-manager budgeted those turns against the 8k fallback (~6.3k usable) and compacted every step (reproduced live:fallback, usable 6349vsrouter, usable 416000). Id-only lookups now resolve when the id is unambiguous across catalog slices; ambiguous/unknown staynull.MOT-3952 — stalled-stream guards
An Anthropic stream stalling mid-
input_json_deltazombied for exactly 300s and died as the opaque "stream ended without a terminal frame": the provider pump pings through silence and the relay's idle budget counted pings as activity. Three layered guards: relay-side, post-content pings no longer reset the idle budget (stall → clearidle past 120000ms, partial preserved — verified live on the next stall); provider-side,read_timeout(120s)on the five cloud providers (llamacpp deliberately skipped: no keepalives, slow prompt eval); harness-side,RouterClient::chatraces the held-open trigger so a pre-writer dispatch failure fails the turn instead of hanging it.MOT-3953 — tool-args evidence + live streaming pane
Unparseable tool args degraded to
null/{}, destroying the evidence and (via the harness target fallback) producing the misleadingfunction_not_found: agent_trigger. Shareddegraded_argumentssalvage now keeps the complete leading fields of partial objects (ƒ state::setidentifiable the moment the first field streams) or carries the malformed text as{"_raw": …}— always replay-safe objects. The harness fails wrapper calls with no resolvable target locally with a teachable error, accumulates rawFunctioncallDeltatext, and rides a bounded_streamingtail on coalesced partials; the console renders a liverequest · streaming…pane so you can watch the command being formed.MOT-3954 — backgrounded sessions re-hydrate
Transcript events subscribe only for the active session and hydration was once-ever, so switching away mid-turn froze entries at stale snapshots forever. Backgrounded conversations are now marked stale on every activation switch; returning re-fetches and folds durable truth over frozen snapshots.
Testing
Summary by CodeRabbit