自発送信 — wire real Hermes-initiated turns to the break-in UI #22

Closed
opened 2026-07-29 17:32:08 +00:00 by aiko · 3 comments
Owner

What to build

HITL — blocked cross-repo. Hermes cannot push an unsolicited turn today (aiko/ollvt-hermes-bridge#16, filed as unscoped and needing its own design pass), and when it can, the trigger name has to reach the client (aiko/ollvt-hermes-bridge#17).

The UI was originally split into #24, which has since been folded back into this issue (see the decision below) — both halves land together here.

  • Real unprompted turns render through the existing break-in UI with no UI changes needed
  • The trigger name on screen is the real job id, end to end from the Hermes scheduler
  • The 局 settings tab lists real cron jobs read-only with a working mute toggle
  • Scheduling stays in Hermes — the client mutes, it does not edit jobs
  • Broadcasts that fired while the client was disconnected are recoverable through the reconnect toast

Open design questions that aiko/ollvt-hermes-bridge#16 must settle first, and which change client behaviour: does an unprompted turn interrupt an active conversation or queue behind it, and is there rate limiting? The break-in band assumes it can arrive at any time; if turns queue instead, the invocation line needs a pending count.

Design reference: docs/design/denpa-receiver/README.md.

Acceptance criteria

  • A real cron turn from Hermes renders as a break-in naming its job id
  • Interrupt-vs-queue behaviour is decided, documented, and matches what the UI shows
  • Cron job list in settings reflects the real schedule; mute suppresses without editing it
  • Missed broadcasts are readable after a reconnect
  • Dev injection affordance from #24 is removed or gated out of release builds

Blocked by


Decision — Hermes owns triggering entirely

All proactive-turn triggering is handled by Hermes. The client decides nothing about when she speaks unprompted.

That closes one of this issue''s two open questions and simplifies the client:

  • No trigger logic, no idle timers, no client-side scheduling. (Note the upstream proactive_speak_prompt in the go-between''s conf.yaml is a different mechanism — OLLVT prompting itself on an idle timer — and is not what this is.)
  • No rate limiting in the client. If she is talking too much, that is tuned in Hermes.
  • The client renders whatever arrives, when it arrives. An unprompted turn is a break-in, full stop.
  • The 局 settings cron list stays read-and-mute only, which was already the design.

Still open, and still owned by aiko/ollvt-hermes-bridge#16: whether a broadcast arriving mid-conversation interrupts or queues. If Hermes only ever sends when it decides the moment is right, the client needs no queue and the break-in band as drawn is correct. If turns can stack, the invocation line needs a pending count that nothing currently draws. Default to no queue unless #16 says otherwise.


Reality audit

Governing rule: build to reality, Hermes leads. Not doing something is acceptable if it does not break core functionality. Breaking the design is acceptable. Building UI that does nothing is not.

Audited against the actual wire surface:

  • Hermes (hermes-agent 0.19.0, verified running): /health, /v1/models, /v1/runs, /v1/runs/{id}, /v1/runs/{id}/events, /v1/runs/{id}/stop, /v1/runs/{id}/approval
  • Run events: message.delta, tool.started, tool.completed, run.completed, run.cancelled, run.failed/run.errored, reasoning.available, approval.request (carries a per-request choices array and sets run status waiting_for_approval; the go-between discards it today — see #19)
  • Go-between → client: full-text, audio, control, error, set-model-and-conf, config-switched, config-files, background-files, history-list, history-data, new-history-created, history-deleted, user-input-transcription, backend-synth-complete, force-new-message, tool_call_status, interrupt-signal, heartbeat-ack, group-update
  • Client → go-between: text-input, mic-audio-data, mic-audio-end, raw-audio-data, ai-speak-signal, interrupt-signal, fetch-history-list, fetch-and-set-history, create-new-history, delete-history, fetch-configs, switch-config, fetch-backgrounds, audio-play-start, request-init-config, heartbeat

#24 folded back in

The client-side break-in UI is no longer a separate issue. Nothing can trigger an unprompted turn today, so building the band, the invocation line and the parked-tab pulse against a dev-injected message would be UI that does nothing.

Both halves land together when aiko/ollvt-hermes-bridge#16 makes a Hermes-initiated turn possible: trigger attribution, the acid break-in band, the pal invocation line, the parked-tab pulse, and the acid reconnect toast variant for broadcasts missed while dark.

Note ai-speak-signal already exists client→server — that is the client asking her to speak, not Hermes initiating. Do not mistake one for the other.


Decision — turns queue, and the invocation line draws a pending count

aiko/ollvt-hermes-bridge#16 has settled the second open question: an unprompted turn queues behind an active conversation rather than interrupting it. The "default to no queue" note above is superseded.

What this changes for the client:

  • The invocation line needs a pending count — how many turns are waiting after the one on screen. This is the one thing the current design does not draw, and it is now required.
  • The break-in band no longer arrives at arbitrary moments. It arrives at turn boundaries only. The band as drawn is otherwise correct.
  • The client still queues nothing. Hermes owns the queue and gates the drain on backend-synth-complete. The client renders what arrives, when it arrives — that part of the earlier decision stands unchanged.
  • User input wins. While the user is talking, the queue holds. No client-side logic is needed for this; it falls out of Hermes gating the drain.

The pending count and the job id arrive on a single new frame, specified as item 4 of aiko/ollvt-hermes-bridge#17.

Missed broadcasts get simpler. The queue is the recovery mechanism: Hermes coalesces per job id (a cron that fired twelve times while the client was dark surfaces once, marked 12x) and applies a per-job TTL to drop stale nudges, then drains on reconnect. The acid reconnect toast announces what is about to drain rather than reconstructing history. The "missed broadcasts are readable after a reconnect" criterion needs no separate machinery.

Both open questions from this issue are now closed. It remains blocked on aiko/ollvt-hermes-bridge#16 for the inbound hook itself.

## What to build **HITL — blocked cross-repo. Hermes cannot push an unsolicited turn today (aiko/ollvt-hermes-bridge#16, filed as unscoped and needing its own design pass), and when it can, the trigger name has to reach the client (aiko/ollvt-hermes-bridge#17).** The UI was originally split into #24, which has since been **folded back into this issue** (see the decision below) — both halves land together here. - Real unprompted turns render through the existing break-in UI with no UI changes needed - The trigger name on screen is the real job id, end to end from the Hermes scheduler - The 局 settings tab lists real cron jobs read-only with a working mute toggle - **Scheduling stays in Hermes** — the client mutes, it does not edit jobs - Broadcasts that fired while the client was disconnected are recoverable through the reconnect toast Open design questions that aiko/ollvt-hermes-bridge#16 must settle first, and which change client behaviour: does an unprompted turn **interrupt an active conversation or queue behind it**, and is there rate limiting? The break-in band assumes it can arrive at any time; if turns queue instead, the invocation line needs a pending count. Design reference: `docs/design/denpa-receiver/README.md`. ## Acceptance criteria - [ ] A real cron turn from Hermes renders as a break-in naming its job id - [ ] Interrupt-vs-queue behaviour is decided, documented, and matches what the UI shows - [ ] Cron job list in settings reflects the real schedule; mute suppresses without editing it - [ ] Missed broadcasts are readable after a reconnect - [ ] Dev injection affordance from #24 is removed or gated out of release builds ## Blocked by - aiko/ollvt-hermes-bridge#16 - aiko/ollvt-hermes-bridge#17 --- ## Decision — Hermes owns triggering entirely **All proactive-turn triggering is handled by Hermes.** The client decides nothing about when she speaks unprompted. That closes one of this issue''s two open questions and simplifies the client: - No trigger logic, no idle timers, no client-side scheduling. (Note the upstream `proactive_speak_prompt` in the go-between''s `conf.yaml` is a *different* mechanism — OLLVT prompting itself on an idle timer — and is not what this is.) - No rate limiting in the client. If she is talking too much, that is tuned in Hermes. - The client **renders whatever arrives, when it arrives.** An unprompted turn is a break-in, full stop. - The 局 settings cron list stays read-and-mute only, which was already the design. Still open, and still owned by aiko/ollvt-hermes-bridge#16: whether a broadcast arriving mid-conversation **interrupts or queues**. If Hermes only ever sends when it decides the moment is right, the client needs no queue and the break-in band as drawn is correct. If turns can stack, the invocation line needs a pending count that nothing currently draws. **Default to no queue** unless #16 says otherwise. --- ## Reality audit Governing rule: **build to reality, Hermes leads. Not doing something is acceptable if it does not break core functionality. Breaking the design is acceptable. Building UI that does nothing is not.** Audited against the actual wire surface: - **Hermes** (`hermes-agent 0.19.0`, verified running): `/health`, `/v1/models`, `/v1/runs`, `/v1/runs/{id}`, `/v1/runs/{id}/events`, `/v1/runs/{id}/stop`, `/v1/runs/{id}/approval` - **Run events**: `message.delta`, `tool.started`, `tool.completed`, `run.completed`, `run.cancelled`, `run.failed`/`run.errored`, `reasoning.available`, `approval.request` (carries a per-request `choices` array and sets run status `waiting_for_approval`; the go-between discards it today — see #19) - **Go-between → client**: `full-text`, `audio`, `control`, `error`, `set-model-and-conf`, `config-switched`, `config-files`, `background-files`, `history-list`, `history-data`, `new-history-created`, `history-deleted`, `user-input-transcription`, `backend-synth-complete`, `force-new-message`, `tool_call_status`, `interrupt-signal`, `heartbeat-ack`, `group-update` - **Client → go-between**: `text-input`, `mic-audio-data`, `mic-audio-end`, `raw-audio-data`, `ai-speak-signal`, `interrupt-signal`, `fetch-history-list`, `fetch-and-set-history`, `create-new-history`, `delete-history`, `fetch-configs`, `switch-config`, `fetch-backgrounds`, `audio-play-start`, `request-init-config`, `heartbeat` ### #24 folded back in The client-side break-in UI is no longer a separate issue. Nothing can trigger an unprompted turn today, so building the band, the invocation line and the parked-tab pulse against a dev-injected message would be UI that does nothing. Both halves land together when aiko/ollvt-hermes-bridge#16 makes a Hermes-initiated turn possible: trigger attribution, the acid break-in band, the pal invocation line, the parked-tab pulse, and the acid reconnect toast variant for broadcasts missed while dark. Note `ai-speak-signal` already exists client→server — that is *the client asking her to speak*, not Hermes initiating. Do not mistake one for the other. --- ## Decision — turns queue, and the invocation line draws a pending count aiko/ollvt-hermes-bridge#16 has settled the second open question: an unprompted turn **queues behind an active conversation** rather than interrupting it. The "default to no queue" note above is superseded. What this changes for the client: - **The invocation line needs a pending count** — how many turns are waiting after the one on screen. This is the one thing the current design does not draw, and it is now required. - **The break-in band no longer arrives at arbitrary moments.** It arrives at turn boundaries only. The band as drawn is otherwise correct. - **The client still queues nothing.** Hermes owns the queue and gates the drain on `backend-synth-complete`. The client renders what arrives, when it arrives — that part of the earlier decision stands unchanged. - **User input wins.** While the user is talking, the queue holds. No client-side logic is needed for this; it falls out of Hermes gating the drain. The pending count and the job id arrive on a single new frame, specified as item 4 of aiko/ollvt-hermes-bridge#17. **Missed broadcasts get simpler.** The queue is the recovery mechanism: Hermes coalesces per job id (a cron that fired twelve times while the client was dark surfaces once, marked `12x`) and applies a per-job TTL to drop stale nudges, then drains on reconnect. The acid reconnect toast announces what is about to drain rather than reconstructing history. The "missed broadcasts are readable after a reconnect" criterion needs no separate machinery. Both open questions from this issue are now closed. It remains blocked on aiko/ollvt-hermes-bridge#16 for the inbound hook itself.
aiko changed title from 自発送信 — unprompted turns that name their trigger to 自発送信 — wire real Hermes-initiated turns to the break-in UI 2026-07-29 17:41:13 +00:00
Author
Owner

Transport decided — Hermes Platform adapter, and this issue gains a second trigger path

ollvt-hermes-bridge#16 settled how an unsolicited turn reaches the client: a real Hermes Platform adapter, built on the plugin path (~/.hermes/plugins/, register_platform() in a register(ctx) entry point, zero changes to core Hermes). Not the go-between polling, which was briefly the plan.

What this changes for this issue:

  • Unprompted turns are no longer cron-only. With the adapter registered, the send_message tool can target Denpa from any context — she can speak because the agent decided to, not merely because a schedule fired. The break-in UI must not assume a job_id exists for every unsolicited turn.
  • Attribution is a tagged union, not a job idcron:morning.brief | webhook:github-pr. Webhook routes (gateway/platforms/webhook.py) are proactive from the client's view and identified by route name. See ollvt-hermes-bridge#17 item 4.
  • deliver: denpa becomes a first-class cron delivery target via cron_deliver_env_var="DENPA_HOME_CHANNEL".
  • Unattributed turns are possible. standalone_sender_fn takes no metadata, so a job fired by a standalone hermes cron tick with the gateway down arrives with no id. Degrade to a generic break-in; do not drop the turn.
  • pending no longer comes from counting unread output files. That derivation belonged to the withdrawn polling design. It now has to ride along from the Hermes-side queue — open sub-question on ollvt-hermes-bridge#16.

Unchanged: Hermes owns triggering and the queue, turns queue rather than interrupt, the client renders what arrives when it arrives.

Correction to this issue's Blocked-by list: 承認 is not part of what ollvt-hermes-bridge#16 unblocks, and never was — see #26. It has had a complete data path on /v1/runs all along.

## Transport decided — Hermes Platform adapter, and this issue gains a second trigger path `ollvt-hermes-bridge#16` settled how an unsolicited turn reaches the client: **a real Hermes Platform adapter**, built on the plugin path (`~/.hermes/plugins/`, `register_platform()` in a `register(ctx)` entry point, zero changes to core Hermes). Not the go-between polling, which was briefly the plan. What this changes for this issue: - **Unprompted turns are no longer cron-only.** With the adapter registered, the `send_message` tool can target Denpa from any context — she can speak because the agent decided to, not merely because a schedule fired. The break-in UI must not assume a `job_id` exists for every unsolicited turn. - **Attribution is a tagged union, not a job id** — `cron:morning.brief` | `webhook:github-pr`. Webhook routes (`gateway/platforms/webhook.py`) are proactive from the client's view and identified by route name. See `ollvt-hermes-bridge#17` item 4. - **`deliver: denpa`** becomes a first-class cron delivery target via `cron_deliver_env_var="DENPA_HOME_CHANNEL"`. - **Unattributed turns are possible.** `standalone_sender_fn` takes no metadata, so a job fired by a standalone `hermes cron tick` with the gateway down arrives with no id. Degrade to a generic break-in; do not drop the turn. - **`pending` no longer comes from counting unread output files.** That derivation belonged to the withdrawn polling design. It now has to ride along from the Hermes-side queue — open sub-question on `ollvt-hermes-bridge#16`. Unchanged: Hermes owns triggering and the queue, turns queue rather than interrupt, the client renders what arrives when it arrives. **Correction to this issue's Blocked-by list:** 承認 is not part of what `ollvt-hermes-bridge#16` unblocks, and never was — see #26. It has had a complete data path on `/v1/runs` all along.
Author
Owner

Blocked-by list is stale

#24 is still listed as a blocker but is closed — folded back into this issue by the "#24 folded back in" decision above. Both halves land together here, so the entry should go.

Remaining real blockers are ollvt-hermes-bridge#16 (the Platform adapter and the go-between's inbound receiver) and ollvt-hermes-bridge#17 item 4 (the trigger + pending frame).

Also: the run-events list in this issue's reality audit omits approval.request, which Hermes 0.19.0 does emit. It does not affect this issue's scope — 承認 is tracked at #19 — but the pasted audit block is a snapshot, not an authority, and it appears in several sibling issues with the same omission.

## Blocked-by list is stale `#24` is still listed as a blocker but is **closed** — folded back into this issue by the *"#24 folded back in"* decision above. Both halves land together here, so the entry should go. Remaining real blockers are `ollvt-hermes-bridge#16` (the Platform adapter and the go-between's inbound receiver) and `ollvt-hermes-bridge#17` item 4 (the `trigger` + `pending` frame). Also: the run-events list in this issue's reality audit omits `approval.request`, which Hermes 0.19.0 does emit. It does not affect this issue's scope — 承認 is tracked at #19 — but the pasted audit block is a snapshot, not an authority, and it appears in several sibling issues with the same omission.
Author
Owner

Closing in favour of #32, rewritten against the ADRs.

This issue's premise changed with ADR-0001: Denpa is a Hermes gateway Platform adapter and the api_server HTTP surface is abandoned entirely. Patching the body would have left a trail of corrections on top of a stale audit block - the same pattern that got 承認 wrongly cut once already.

Decisions now live in docs/adr/ in the go-between repo (branch docs/hermes-platform-adrs). The replacement issue references them rather than restating them.

Closing in favour of #32, rewritten against the ADRs. This issue's premise changed with ADR-0001: Denpa is a Hermes gateway Platform adapter and the `api_server` HTTP surface is abandoned entirely. Patching the body would have left a trail of corrections on top of a stale audit block - the same pattern that got 承認 wrongly cut once already. Decisions now live in `docs/adr/` in the go-between repo (branch `docs/hermes-platform-adrs`). The replacement issue references them rather than restating them.
aiko closed this issue 2026-07-30 00:03:52 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiko/denpa#22
No description provided.