Skip to content

feat(harness,console): push queued-message events, drop the status poll (MOT-3837) - #452

Merged
andersonleal merged 31 commits into
mainfrom
feat/queued-message-push
Jul 9, 2026
Merged

feat(harness,console): push queued-message events, drop the status poll (MOT-3837)#452
andersonleal merged 31 commits into
mainfrom
feat/queued-message-push

Conversation

@andersonleal

@andersonleal andersonleal commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

What

Removes the console's 2.5s harness::status poll for the queued-messages strip and replaces it with a push signal.

  • harness: new harness::message-queued trigger type (third sibling of harness::turn-started/turn-completed, same binding config and fan-out), emitted after a message parks in the mid-turn harness_queue. Payload is a pointer (session_id, entry_id, queued_at), not the message.
  • console: turn-events-live.ts gains startQueuedEventsSubscription; ChatView drops the setInterval and refetches harness::status → queued once per event (plus one catch-up fetch on stream start). Refetch-on-signal keeps the strip correct under at-least-once delivery.
  • docs: new trigger type specced in harness.md § Trigger types emitted.

Stacking note

feat/message-queue (MOT-3837, the queue itself) exists only locally, so this PR bases on main and includes those commits — my change is the last commit (f7a9b35). Land the queue work first, then this rebases to just the one commit.

Related tickets

Because feat/message-queue rides along locally, this branch bundles the queue and a batch of unrelated console/harness work. Split into focused tickets for tracking:

  • MOT-3837 — queue messages + notifications while streaming; mid-stream queueing, editing, and in-place edit (the queue work).
  • MOT-3920 — triggers strip: collapse, inspect reaction spec, DAG flow view.
  • MOT-3921 — readable engine / function-call payloads in the console (double-encoded JSON, blank functions::info tab, rich triggers::info, register_trigger when→then).
  • MOT-3922 — harden reactive pipelines against silent fail-closed stalls (spawn-policy prompt guards across harness + 6 providers, unwritten-state-key annotation, react-task-vs-user-message fix).

Checks

  • cargo test (harness): 189 passed; cargo fmt --check + clippy clean
  • console: tsc -b --noEmit clean; vitest turn-events-live 7/7 (2 new cases for the queued binding)
  • biome: touched lines clean; 3 pre-existing findings in ChatView.tsx come from the base branch

Not done (deliberate)

  • The 5s triggers-list poll in ChatView stays — there's no register/unregister event to bind to today.
  • No reconnect/keepalive fallback: a missed event only leaves the strip momentarily stale, and stream-end drains everything into the transcript anyway.

…-3837)

Messages and subagent notifications arriving while a turn streams now land
in a durable per-session queue (harness_queue state scope) instead of being
appended mid-stream or racing the in-flight generation. The loop drains the
queue at the start of the next step, in arrival order, under the deterministic
entry ids the rows were queued with, so redelivery is idempotent.

- harness::send / harness::inject enqueue instead of appending while a turn
  is Running; parked (awaiting_functions) turns still append immediately.
- Steering now treats any non-Custom queued row like a trailing user message,
  closing a gap where a custom-role message merged into a running turn could
  be silently dropped if the final step made no function calls.
- finalize_* drains best-effort on completion/failure/cancellation so a
  message that arrives as the turn dies is never stranded.
- harness::status now reports the session's queued rows.
harness::react already fell back to the registering (owner) session when a
join's downstream spec omitted `session_id` — a fan-in result landed as a
turn in the chat that wired it instead of a detached child nobody reads.
That fallback (`join_delivery_session`) was only ever invoked from the
join-satisfaction path, though: a plain, single-predecessor reaction (no
join) with no `session_id` pin dispatched the raw, unresolved spec, so
`harness::spawn` minted a fresh anonymous session instead.

Reproduced live: a pipeline stage registered a turn-completed reaction with
no session_id, explicitly intending its result to land back in the
registering session — instead it was stranded in an orphaned `s_...` session
nobody was watching.

Resolve the owner fallback once, before the join/non-join split, so both
dispatch paths share it. Renamed join_delivery_session ->
reaction_delivery_session to reflect the now-general use.
Unlocks the composer during streaming instead of just showing a stop button:
a send while the turn is live queues the message on the harness (no second
stream loop) and shows it as a draft chip above the composer until its
drained row lands in the transcript. The strip also polls harness::status so
it reflects server-side queue rows from other tabs and subagent/subscription
notifications, not just this tab's own drafts.
Surfaces the notify/react bindings a session has registered via the
harness's engine::register_trigger intercept as a strip above the composer:
unregister inline, or open a detail dialog (fires on, delivers to, lifetime,
subscription id, raw config/metadata). When enough triggers form a spawn
chain or join group, renders them as a staged workflow view instead of a
flat list, grouping join predecessors under one unit and leveling
spawn -> watch edges so dependencies are visible at a glance.
applyCatalogModelFallback assigned the catalog's first model to every
conversation with a null model once the catalog loaded, including sessions
discovered from the engine (sub-agents, other surfaces) that never had a
console-side model choice to begin with — so a sub-agent running e.g.
claude-sonnet-5 would display whatever model happened to be first in the
picker. Only console drafts get the catalog default now; a discovered
session's null model is left alone.

ChatView derives the display model from the transcript instead: when
conversation.model is null, effectiveModel falls back to the model the
latest assistant reply actually used, resolved against the catalog's
composite ids so the header and picker still preselect correctly. Also
feeds handleSubmit and the context-window estimate, so steering a
discovered session inherits its real model instead of hitting "select a
model".
…ll (MOT-3837)

The console's queued strip polled harness::status every 2.5s while a
step streamed to see rows parked by other tabs and subagent
notifications. The harness now emits a harness::message-queued trigger
after a message parks in the mid-turn queue; the console binds it (same
pattern as harness::turn-completed) and refetches the queue once per
event. The payload is a pointer, not the message, so the refetch stays
idempotent under at-least-once delivery.
@vercel

vercel Bot commented Jul 8, 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 9, 2026 12:19pm
workers-tech-spec Ready Ready Preview, Comment Jul 9, 2026 12:19pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@andersonleal, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5c253ca6-e078-4b50-b4d8-4955ed94378a

📥 Commits

Reviewing files that changed from the base of the PR and between 376f127 and 8550256.

⛔ Files ignored due to path filters (2)
  • provider-anthropic/Cargo.lock is excluded by !**/*.lock
  • provider-openai/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (50)
  • console/web/src/components/chat/ChatView.tsx
  • console/web/src/components/chat/Composer.tsx
  • console/web/src/components/chat/LexicalShell.tsx
  • console/web/src/components/chat/Message.tsx
  • console/web/src/components/chat/SessionTriggers.test.ts
  • console/web/src/components/chat/SessionTriggers.tsx
  • console/web/src/components/chat/TriggerDag.tsx
  • console/web/src/components/chat/engine/FunctionInfoView.tsx
  • console/web/src/components/chat/engine/RegisterTriggerView.tsx
  • console/web/src/components/chat/engine/TriggerInfoView.tsx
  • console/web/src/components/chat/engine/__tests__/parsers.test.ts
  • console/web/src/components/chat/engine/index.tsx
  • console/web/src/components/chat/engine/parsers.ts
  • console/web/src/components/chat/queue-history.test.ts
  • console/web/src/components/chat/queue-history.ts
  • console/web/src/components/chat/trigger-graph.test.ts
  • console/web/src/components/chat/trigger-graph.ts
  • console/web/src/components/function-call/FunctionCallCard.test.ts
  • console/web/src/components/function-call/FunctionCallCard.tsx
  • console/web/src/hooks/use-conversations.test.ts
  • console/web/src/hooks/use-conversations.ts
  • console/web/src/lib/backend/harness-send.ts
  • console/web/src/lib/backend/real.ts
  • console/web/src/lib/backend/triggers.ts
  • console/web/src/lib/backend/turn-events-live.test.ts
  • console/web/src/lib/backend/turn-events-live.ts
  • console/web/src/lib/backend/types.ts
  • console/web/src/lib/sessions/entry-mapper.test.ts
  • console/web/src/lib/sessions/entry-mapper.ts
  • console/web/src/types/chat.ts
  • console/web/src/types/iii-agent-event.ts
  • harness/prompts/cli.txt
  • harness/prompts/default.txt
  • harness/src/events.rs
  • harness/src/functions/mod.rs
  • harness/src/functions/react.rs
  • harness/src/functions/send.rs
  • harness/src/functions/status.rs
  • harness/src/ids.rs
  • harness/src/state.rs
  • harness/src/subagent.rs
  • harness/src/turn_loop.rs
  • harness/tests/golden/schemas/harness.send.json
  • harness/tests/golden/schemas/harness.status.json
  • provider-anthropic/prompts/identity.txt
  • provider-llamacpp/prompts/identity.txt
  • provider-openai-codex/prompts/identity.txt
  • provider-openai/prompts/identity.txt
  • provider-xai/prompts/identity.txt
  • provider-zai/prompts/identity.txt
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/queued-message-push

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.

@github-actions

github-actions Bot commented Jul 8, 2026

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.

The registered-triggers strip now collapses to a count header
("N triggers registered · M stages") and expands on click. The bare
↓ divider between workflow stages now names the dependency — "after
<session> completes" — derived from the sessions the stage's units
watch, so chained triggers read as an explicit pipeline.
harness::react bindings previously read "spawns sub-agent" in the row
and dumped the raw spec JSON in the dialog. Now:

- rows and join headers show the reaction's model; hovering a react row
  previews its task
- the dialog gets model (+provider), spawns-into (target session or the
  owner chat), and join (waits-for / fires-as / re-arms) rows, plus the
  task prompt as its own readable section; the JSON block shrinks to the
  true leftovers (spawn options)
- react bindings stamp __once, not once — the console now reads both,
  so a once-reaction's lifetime no longer shows "until unregistered"
Post-mortem of a stalled state-coordinated pipeline (sub-agents spawned
without state::set finished politely, leaving every once-reaction armed
on keys nothing could write). Three guards:

1. Agent prompts (harness default/cli + all six provider identities):
   a denied required function means the task FAILED — report it as the
   first line, never bury it under deliverable-looking output.
2. Same prompts: children run fail-closed from a read-mostly baseline —
   grant whatever the task requires via options.functions.allow; plus a
   final-checklist item to verify every armed reaction has a producer
   that can actually produce the watched key/event.
3. Console triggers strip: state bindings now show their watched key
   and whether it exists ("on wiki-pipeline/summary — not written
   yet"), probed via state::get while the strip is expanded, so a
   pipeline armed on an unwritable key is diagnosable at a glance.
A reaction delivered into a chat (harness::react → spawn into the owner
session) appended its task as a bare user entry, so the console showed
the machine-sent prompt as '$ you' — as if the human typed it.

Mirror the notify pattern end to end: the harness stamps react-fired
task appends with origin { reaction: true, subscription_id? } and an
e_react_ entry id (reads carry no origin; the prefix is the read-path
fallback), and the console maps either signal to a reaction flag that
renders as '⚡ trigger · reaction task', left-aligned, instead of a
user bubble. reactive_depth gates the stamp — only harness::react sets
it, and the dispatch path clears model-supplied values, so a real user
message can never be mislabeled.
# Conflicts:
#	console/web/src/components/chat/Composer.tsx
#	harness/src/functions/send.rs
@andersonleal

Copy link
Copy Markdown
Collaborator Author

Merged origin/main in (d5dc67eb) to pick up #448, which had landed on main and touched several of the same files as this branch.

Two real conflicts, both resolved by hand:

  • Composer.tsx: feat(console): ux improvements #448 redesigned the toolbar (folded the thinking-level picker into ModelPicker, moved AttachmentButton after the model picker, switched to icon-only stop/send buttons). This branch's mid-stream-queue work (queueWhileStreaming, the optionsDisabled/inputDisabled split, the extra queue-send button) predates that redesign. Resolution: kept feat(console): ux improvements #448's new layout and combined ModelPicker, re-added the queue-send button in the new position, preserved the optionsDisabled locking semantics for the picker row.
  • harness/src/functions/send.rs: feat(console): ux improvements #448 added a message_preview field threaded through seed_or_merge/seed_new (an OTel trace-tag improvement). This branch added the try_enqueue queue-path branching. Resolution: kept both — the queue path now also computes and passes message_preview on its non-queued (append) branch, matching feat(console): ux improvements #448's new signature.

Verified post-merge: cargo build + cargo test (190/190) clean, console tsc/vitest (856/856) clean, biome clean.

…d JSON

The <event>/<inputs> block harness::react appends to a reaction task
rendered as mangled inline prose (escaped JSON mid-markdown). The entry
mapper now splits the trailing block off the task text: the task renders
as clean markdown, and the payload becomes a collapsed details row —
'firing event · reviewer-cr7k2 · completed · show json' — expanding to
pretty-printed, syntax-highlighted JSON (join inputs show predecessor
keys instead). Unparseable payloads render raw rather than disappearing.
A message queued mid-stream (MOT-3837) is committed to the server queue
immediately, so editing it means pulling it back out — otherwise the old
version still drains and you get a duplicate. Add that primitive and the
composer ergonomics:

- harness::unqueue (trusted, off the agent catalog): removes a still-
  parked queued row by its client-visible entry_id (resolved to the
  internal row id for deletion). Best-effort — an already-drained row is
  removed:false.
- backend.removeQueued → harness::unqueue.
- LexicalShell: Up-arrow in an EMPTY editor recalls a message for
  editing (gated on empty so an in-progress draft is never clobbered;
  defers to an open typeahead). Loads the returned text, caret at end.
- ChatView: recall the most recent queued draft — drop the local draft,
  remove the server row (surfacing a warn notice if removal fails, since
  the strip also re-shows it), hand text + attachments to the composer.
  Only wired when the backend can remove queued rows (recall without
  removal would double-deliver). A '↑ to edit' hint sits under the strip.

Attachments are display-only (content rides the text's #file mentions),
so recalling them is safe. Registered off-catalog; the golden catalog
test is untouched.
Turns the destructive one-shot recall into non-destructive browsing so
Down can cycle back. ↑ goes older (from a blank composer, enters at the
newest); ↓ goes newer (past the newest, exits to a blank draft). The
message leaves the queue only when the edit is submitted (harness::unqueue
via onCommitEdit) — browsing never removes anything.

- queue-history.ts: pure nextHistoryTarget(list, browseId, current, dir)
  with the cursor + pristine-gate logic, unit-tested (enter / older /
  newer / clamp / exit / edited-protection).
- Pristine gate: navigation only fires when the editor is blank or holds
  the browsed message unedited, so an in-progress edit and caret moves
  within a real edit are never clobbered — no Lexical caret-boundary
  probing needed.
- LexicalShell: HistoryNavPlugin binds ↑ and ↓ (was ↑-only), defers to an
  open typeahead, loads the returned text ('' clears to a live draft).
- The queued strip highlights the message being edited ('editing', accent)
  and the hint switches to '↑/↓ cycle · enter saves · ↓ past newest cancels'.

Submitting an edit re-queues the text at the tail (remove + re-add);
preserving queue position would need an in-place update — deferred.
…tion

Editing a queued message used to remove + re-queue, which moved it to
the tail of the delivery order. Update it in place instead.

- harness::edit_queued (trusted, off-catalog): find the row by entry_id
  and rewrite its message, keeping the same internal id — so re-writing
  the same session_id:id state key overwrites in place, preserving
  queued_at (position), entry_id, and origin. Emits harness::message-queued
  so other tabs refetch the new content.
- backend.editQueued rebuilds the message exactly as a send does (string
  sugar, or structured with #file() expansions) via an extracted
  buildMessageInput.
- Composer: submitting a browsed message now saves it in place
  (onEditQueued) rather than sending a new one; submitting an emptied
  composer removes it (onEditQueued(id, null) → harness::unqueue). ChatView
  updates the draft optimistically and re-expands mentions for the server
  write.
- Strip hint: 'enter saves in place · empty + enter removes'.

The prior remove-then-resend path (and the tail reorder it caused) is
gone; browsing an unedited message and hitting enter is now a no-op move.
While a queued message is being edited it lives only in the composer
now — hidden from the queue strip instead of shown in both places
(strip row + text input). It reappears at its position when saved in
place (edit_queued), or when you cycle away to another message. Pure
display filter: the ↑/↓ cycle source and the save-in-place / remove
behavior are unchanged.

Claude-Session: https://claude.ai/code/session_015tUsXKB3QY6udtEWPiQAA1
The catch handlers swallowed the underlying error and showed a generic
"could not remove/save the queued message" warning, hiding the actual
cause — most notably a stale harness binary that predates
harness::unqueue / harness::edit_queued, which the engine rejects as an
unknown function. Include the error text so the notice self-diagnoses
instead of looking like a silent failure.

Claude-Session: https://claude.ai/code/session_015tUsXKB3QY6udtEWPiQAA1
The main merge (f9fae14) bumped llm-router to 1.0.5 (Cargo.toml +
llm-router/Cargo.lock) but left these two providers' lockfiles pinned at
1.0.4; a build regenerated them to match the declared version. Lockfile
sync only, no code change.

Claude-Session: https://claude.ai/code/session_015tUsXKB3QY6udtEWPiQAA1
Drops this branch's edits to tech-specs/2026-06-agentic/harness.md (the
MOT-3837 queue + harness::message-queued trigger doc additions),
reverting the file to origin/main so this PR no longer touches the
published spec site.

Claude-Session: https://claude.ai/code/session_015tUsXKB3QY6udtEWPiQAA1
CI clippy (--all-targets, -D warnings, rust 1.96) flagged
`let rows = vec![...]` in a test that only iterates it. A plain
`cargo clippy` run skips test targets, so it slipped through locally.
Use an array literal; behaviour unchanged.

Claude-Session: https://claude.ai/code/session_015tUsXKB3QY6udtEWPiQAA1
@andersonleal
andersonleal merged commit a51b5a0 into main Jul 9, 2026
99 of 100 checks passed
andersonleal added a commit that referenced this pull request Aug 12, 2026
… drop the console trigger poll (#775)

* (MOT-4401) feat(harness,console): push trigger-binding change events, drop the console trigger poll

Every open console chat tab polled harness::triggers::list on a 5s
interval (each call a full-scope harness::state::list on the state
worker). Replace the poll with a harness::triggers-changed doorbell —
the same pattern #452 used to kill the status poll:

- harness: 5th event trigger type; {session_id, timestamp} emitted from
  the four BindingStore CAS primitives (reserve, attach_trigger_id,
  claim_fire, delete_if_unchanged), so every mutation path incl. the
  out-of-turn ones (expiry sweep, GC, other-tab unregister, deferred
  release) rings from one layer.
- console: onTriggersChanged backend hook (shape of onQueuedMessage);
  ChatView swaps the interval for initial fetch + doorbell subscription
  + visibilitychange catch-up. Always-on for the selected conversation,
  unlike the streaming-gated queued subscription.

Version skew is graceful both ways: an old harness parks the unknown
trigger-type registration (console degrades to mount/visibility/post-
unregister refetches); an old console keeps polling a new harness.

* (MOT-4401) fix(console): coalesce trigger refetches, close subscription gaps

Review fixes on #775:

- serialRefresh: doorbells arrive at-least-once and burst on rapid fires;
  refetches now serialize behind one in-flight list read with a single
  trailing rerun, so an older snapshot can never resolve after and
  overwrite a newer one. reset() discards in-flight responses on
  conversation switch/unmount (also fixes the pre-existing late-write
  into a switched conversation).
- ChatView subscribes to the doorbell BEFORE the initial snapshot, so a
  mutation in the setup gap rings instead of being missed.
- realOnTriggersChanged reseeds on every 'connected' transition: the SDK
  replays trigger registrations on reconnect but outage-window doorbells
  are gone (same repair TracesV2 uses). The reseed is ordered after the
  SDK's synchronous registration replay, so it also covers the first
  connect.
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