Skip to content

Trigger-fired visibility, stream-stall guards, and console reliability (MOT-3949…MOT-3954) - #468

Merged
andersonleal merged 11 commits into
mainfrom
andersonleal/trigger-fired-visibility
Jul 10, 2026
Merged

Trigger-fired visibility, stream-stall guards, and console reliability (MOT-3949…MOT-3954)#468
andersonleal merged 11 commits into
mainfrom
andersonleal/trigger-fired-visibility

Conversation

@andersonleal

@andersonleal andersonleal commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

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 once trigger 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-invisible trigger_fired custom 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 fired once triggers in the panel as dismissable "fired · unregistered" ghosts (workflow DAG survives retirement). Child sessions carry spawned_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 drops router::ready bindings when the router disconnects. Now: restore resets to false, a successful chat heals it back (+ provider::changed), and the router boot-nudges each known provider's on_router_ready directly. Console disables models of unavailable providers ("not loaded") and refreshes on provider changes. Chat error surfacing: session::messages read-backs deliver custom entries as role:"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::get with an empty provider returned null, so context-manager budgeted those turns against the 8k fallback (~6.3k usable) and compacted every step (reproduced live: fallback, usable 6349 vs router, usable 416000). Id-only lookups now resolve when the id is unambiguous across catalog slices; ambiguous/unknown stay null.

MOT-3952 — stalled-stream guards

An Anthropic stream stalling mid-input_json_delta zombied 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 → clear idle 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::chat races 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 misleading function_not_found: agent_trigger. Shared degraded_arguments salvage now keeps the complete leading fields of partial objects (ƒ state::set identifiable 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 raw FunctioncallDelta text, and rides a bounded _streaming tail on coalesced partials; the console renders a live request · 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

  • harness: 204 tests, clippy/fmt clean (incl. new fired-record, relay-race, streaming-tail, wrapper-sentinel tests)
  • llm-router: 76 tests incl. restart-availability integration, ping/idle relay test, salvage unit tests
  • providers ×6: suites green incl. malformed-args evidence and partial-salvage pins
  • console: 921 vitest + tsc clean (mapper wire-shape regression, merge/ghost tests, re-hydration tests)
  • Live-verified on the pipeline rig: fired ghosts + notices, availability recovery, compaction stopped, stall died at 120s with a named reason, streaming pane

Summary by CodeRabbit

  • New Features
    • Added durable trigger-fired notices (including join progress) with preserved fired-trigger history, plus spawn/agent origin indicators and UI for spawn tasks.
    • Streamed function-call arguments now show a live “streaming” request pane; model/provider “not loaded” availability is shown in the picker.
  • Bug Fixes
    • Improved trigger history merging with local dismissal and better stream/idle handling; enhanced message rendering for thinking and malformed tool-call arguments via safer JSON replay.
    • Improved provider availability healing after completed streams.
  • Tests
    • Expanded coverage for trigger merging, hydration/staleness, streaming tails, provider availability, and JSON degradation behavior.

…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)
@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jul 10, 2026 5:59pm
workers-tech-spec Ready Ready Preview, Comment Jul 10, 2026 5:59pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 41 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Trigger lifecycle and spawn provenance

Layer / File(s) Summary
Durable trigger events and spawn metadata
harness/src/subscriptions/*, harness/src/functions/react.rs, harness/src/subagent.rs, harness/src/trigger.rs, harness/src/turn_loop.rs
Trigger and notification executions persist fired records, child sessions record their origin, and unresolved or degraded trigger calls produce normalized results.
Transcript contracts and typed message mapping
console/web/src/types/chat.ts, console/web/src/lib/sessions/entry-mapper.ts, console/web/src/components/chat/Message.tsx
Typed custom entries map to trigger-fired notices and spawn task messages, while streaming wrapper arguments are represented in transcript rows.
Fired-trigger reconciliation and trigger panel behavior
console/web/src/lib/backend/triggers.ts, console/web/src/components/chat/ChatView.tsx, console/web/src/components/chat/SessionTriggers.tsx
Polled triggers merge with durable fired history, preserving ghost rows and metadata while allowing fired rows to be dismissed locally.
Conversation provenance and background hydration
console/web/src/hooks/use-conversations.ts, console/web/src/hooks/use-conversations.test.ts, console/web/src/components/sidebar/ConversationRow.tsx
Conversation metadata maps spawn origin, live transcript updates are buffered during hydration, background sessions become stale, and child-origin icons are displayed.

Streaming lifecycle and partial tool calls

Layer / File(s) Summary
Router stream completion and partial arguments
harness/src/clients/router.rs
Frame consumption observes trigger completion, performs a bounded drain, exposes streaming argument tails, and classifies dispatch errors.
Streaming transcript and console rendering
console/web/src/lib/sessions/entry-mapper.ts, console/web/src/components/function-call/FunctionCallCard.tsx, console/web/src/components/chat/ChatView.tsx
Incomplete wrapper arguments become intermediate call rows, streaming tails render in a dedicated pane, and errored conversations abort local stream loops.
Thinking status presentation
console/web/src/components/chat/MessageList.tsx, console/web/src/components/chat/ChatView.tsx
Thinking shimmer text uses conversation status details or the effective model.

Provider availability and catalog updates

Layer / File(s) Summary
Provider availability lifecycle
llm-router/src/register.rs, llm-router/src/registry/store.rs, llm-router/src/chat/chat.rs, llm-router/tests/integration.rs
Restored providers start unavailable, readiness is replayed, successful streams restore availability, and integration tests verify both states.
Catalog subscriptions and model resolution
console/web/src/lib/models-catalog.ts, console/web/src/hooks/use-model-picker-source.ts, llm-router/src/catalog/*
Provider changes trigger catalog reloads, provider availability is exposed in catalog entries, and provider-less model lookup requires a unique model id.
Unavailable provider picker state
console/web/src/components/chat/ModelPicker.tsx
Unavailable provider groups display “not loaded” and their model items are disabled.

Stream timeouts and degraded arguments

Layer / File(s) Summary
Relay idle and upstream read budgets
llm-router/src/chat/relay.rs, provider-*/src/register.rs
Relay idle timing distinguishes pre-content keepalives from post-content progress, and provider clients use bounded read timeouts.
Degraded tool-argument reconstruction
llm-router/src/types/messages.rs
Malformed JSON is converted into salvageable leading fields or bounded _raw evidence.
Provider SSE argument preservation
provider-*/src/sse.rs, provider-anthropic/src/wire/tools.rs
Provider adapters preserve malformed and partial function-call arguments, and Anthropic tool definitions enable eager input streaming.

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
Loading
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
Loading

Possibly related PRs

  • iii-hq/workers#401: Adds the complementary backend reactive-trigger bridge and session-spawning infrastructure.
  • iii-hq/workers#442: Covers the related durable trigger_fired lifecycle and console reconciliation.

Suggested reviewers: ytallo

Poem

I’m a rabbit with triggers to tend,
Fired notes hop where old rows would end.
Streams show their tails, providers shine,
Spawned agents leave footprints in line.
JSON keeps scraps instead of turning to dust.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the main themes of the changeset: trigger-fired UI, stream-stall safeguards, and console reliability.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch andersonleal/trigger-fired-visibility

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Stale listTriggers response can pollute the wrong conversation's trigger cache/panel.

refreshTriggers has 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-reset seenTriggersRef.current and into setSessionTriggers, showing A's triggers (and permanently retaining A's fired-ghost rows) under B. The sibling listQueued effect a few lines above already guards this exact class of race with an alive flag — 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 win

Consider 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. Moving tokio::spawn inside 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

📥 Commits

Reviewing files that changed from the base of the PR and between 03c6de9 and 3b5d677.

📒 Files selected for processing (45)
  • console/web/src/components/chat/ChatView.tsx
  • console/web/src/components/chat/Message.tsx
  • console/web/src/components/chat/MessageList.tsx
  • console/web/src/components/chat/ModelPicker.tsx
  • console/web/src/components/chat/SessionTriggers.tsx
  • console/web/src/components/function-call/FunctionCallCard.tsx
  • console/web/src/components/sidebar/ConversationRow.tsx
  • console/web/src/hooks/use-conversations.test.ts
  • console/web/src/hooks/use-conversations.ts
  • console/web/src/hooks/use-model-picker-source.ts
  • console/web/src/lib/backend/triggers.test.ts
  • console/web/src/lib/backend/triggers.ts
  • console/web/src/lib/models-catalog.ts
  • console/web/src/lib/sessions/entry-mapper.test.ts
  • console/web/src/lib/sessions/entry-mapper.ts
  • console/web/src/types/chat.ts
  • harness/src/clients/router.rs
  • harness/src/functions/react.rs
  • harness/src/ids.rs
  • harness/src/policy.rs
  • harness/src/subagent.rs
  • harness/src/subscriptions/fired.rs
  • harness/src/subscriptions/mod.rs
  • harness/src/subscriptions/notify_agent.rs
  • harness/src/trigger.rs
  • harness/src/turn_loop.rs
  • llm-router/src/catalog/handlers.rs
  • llm-router/src/catalog/queries.rs
  • llm-router/src/chat/chat.rs
  • llm-router/src/chat/relay.rs
  • llm-router/src/register.rs
  • llm-router/src/registry/store.rs
  • llm-router/src/types/messages.rs
  • llm-router/tests/integration.rs
  • provider-anthropic/src/register.rs
  • provider-anthropic/src/sse.rs
  • provider-llamacpp/src/sse.rs
  • provider-openai-codex/src/register.rs
  • provider-openai-codex/src/sse.rs
  • provider-openai/src/register.rs
  • provider-openai/src/sse.rs
  • provider-xai/src/register.rs
  • provider-xai/src/sse.rs
  • provider-zai/src/register.rs
  • provider-zai/src/sse.rs

Comment thread harness/src/clients/router.rs
Comment thread harness/src/trigger.rs Outdated
Comment thread provider-llamacpp/src/sse.rs Outdated
Comment thread provider-openai-codex/src/sse.rs Outdated
Comment thread provider-openai/src/sse.rs Outdated
Comment thread provider-xai/src/sse.rs Outdated
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
console/web/src/hooks/use-conversations.ts (1)

393-396: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the duplicated spawned_by mapping.

The md.spawned_by === 'trigger' || md.spawned_by === 'agent' ? md.spawned_by : ... mapping is duplicated here and in conversationFromMeta (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

📥 Commits

Reviewing files that changed from the base of the PR and between 3b5d677 and 30b1531.

📒 Files selected for processing (13)
  • console/web/src/components/chat/ModelPicker.tsx
  • console/web/src/hooks/use-conversations.ts
  • harness/src/clients/router.rs
  • harness/src/functions/react.rs
  • harness/src/trigger.rs
  • harness/src/turn_loop.rs
  • llm-router/src/types/messages.rs
  • provider-anthropic/src/request.rs
  • provider-llamacpp/src/sse.rs
  • provider-openai-codex/src/sse.rs
  • provider-openai/src/sse.rs
  • provider-xai/src/sse.rs
  • provider-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

Comment thread provider-anthropic/src/request.rs Outdated
…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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
provider-anthropic/src/register.rs (1)

109-128: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider 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

📥 Commits

Reviewing files that changed from the base of the PR and between 30b1531 and 269bf27.

📒 Files selected for processing (14)
  • console/tests/integration.rs
  • console/web/src/hooks/use-conversations.test.ts
  • console/web/src/hooks/use-conversations.ts
  • harness/src/functions/react.rs
  • harness/src/functions/subscribe.rs
  • harness/src/policy.rs
  • harness/src/subscriptions/notify_agent.rs
  • provider-anthropic/src/register.rs
  • provider-anthropic/src/request.rs
  • provider-anthropic/src/wire/tools.rs
  • provider-openai-codex/src/register.rs
  • provider-openai/src/register.rs
  • provider-xai/src/register.rs
  • provider-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

@andersonleal
andersonleal merged commit bc61457 into main Jul 10, 2026
54 checks passed
@andersonleal
andersonleal deleted the andersonleal/trigger-fired-visibility branch July 10, 2026 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant