Files
turnstone/tests
Patrick Buckley 164f74dead feat(operator-context): deliver structured per-kind meta to the UI
Operator-context system turns (watch results, output-guard findings, idle
children, user interjections) carried their kind (_source) and a flattened
text content, but the structured per-kind fields were dropped at every
persist/deliver boundary — so the UI rendered every kind as one generic
operator bubble and the structured watch-result card was lost.

Wire the structured meta through as the single source of truth:

- Storage: new conversations.meta JSON column (migration 060); threaded
  through save_message/save_messages_bulk (facade + protocol + both backends)
  and rehydrated in reconstruct_turns onto Turn.meta.extra["source_meta"].
- Canonical: make_system_turn carries meta as one _source_meta dict;
  turn_from_dict/turn_to_dict bridge it to/from Turn.meta.extra.
- Live + history: widen on_system_turn(content, source, meta) across all
  impls + the SSE payload; surface _source_meta -> meta in the /history
  projection. SDK HistoryEvent docs note the field.
- Producers derive both the model-facing content text AND the card from one
  meta dict, so they cannot drift: render_output_guard_text, build_watch_
  reminder carrying output, idle_children and user_interjection metadata.
- Frontend: addSystemContext / renderSystemTurn dispatch by source to the
  watch-result, guard-finding, idle-children, and queued-message cards in
  both the interactive and coordinator panes; every untrusted field renders
  via textContent.

The meta is a leading-underscore key, stripped before the wire (sanitize_
messages and the native mid-conversation path copy only role+content), so the
per-provider wire payloads stay byte-identical. Additive column, no backfill:
operator turns predating it reload as plain text bubbles.
2026-06-04 11:03:13 -07:00
..