{% extends "base.html" %}
{% block title %}Conversation {{ conversation_id[:12] }} - MoralStack{% endblock %}
{% block content %}
{% set ov = timeline.overview or {} %}
{% set ov_max_risk = ov.get('max_risk_score') %}
{# Step 14.6 — Conversation strip: horizontal visual summary of turns #}
{% if timeline.requests %}
Conversation strip
Each rectangle is one turn. Height encodes risk score (taller = higher risk).
Colour encodes final_action (green = NORMAL_COMPLETE, yellow = SAFE_COMPLETE,
red = REFUSE). Orange border = ESCALATED posture. ⚡ = cached from a previous turn.
Hover for details, click to open the request.
{% for r in timeline.requests %}
{% set meta = r.meta_json__parsed or {} %}
{% set state = r.state or {} %}
{% set summary = state.get('state_summary_json__parsed') or {} %}
{% set risk_score = meta.get('risk_score') %}
{% set posture = state.get('posture') or summary.get('last_governance_posture') or meta.get('governance_posture') %}
{% set action = meta.get('final_action') or 'unknown' %}
{% set was_cached = state.get('was_cached') or meta.get('was_cached') %}
{% set cached_from = state.get('cached_from_turn') %}
{% set bar_height = (24 + ((risk_score or 0.0) * 56)) | int %}
{% set action_class = 'conv-strip-cell-' + (action | lower) %}
{% set escalated = (posture == 'ESCALATED') %}
T{{ r.turn_index }}
{% if was_cached %}
⚡
{% endif %}
{% if r.run_id and r.request_id %}
{% endif %}
{% endfor %}
{% for r in timeline.requests %}
{% set state = r.state or {} %}
{% set cached_from = state.get('cached_from_turn') %}
{% if cached_from is not none and r.turn_index is not none %}
←
{% endif %}
{% endfor %}
{% endif %}
Posture timeline
{% if timeline.requests %}
| Turn |
Request |
Action |
Risk |
Posture (out) |
Cached |
Created |
Actions |
{% for r in timeline.requests %}
{% set meta = r.meta_json__parsed or {} %}
{% set state = r.state or {} %}
{% set summary = state.get('state_summary_json__parsed') or {} %}
{% set risk_score = meta.get('risk_score') %}
{% set state_cached_from = state.get('cached_from_turn') %}
| {{ r.turn_index if r.turn_index is not none else '—' }} |
{% if r.run_id and r.request_id %}
{{ r.request_id[:12] }}
{% else %}{{ (r.request_id or '')[:12] }}{% endif %}
|
{{ meta.get('final_action') or '—' }}
|
{% if risk_score is not none %}
{{ "%.3f" | format(risk_score) }}
{% else %}—{% endif %}
|
{% set posture = state.get('posture') or summary.get('last_governance_posture') or meta.get('governance_posture') %}
{% if posture %}{{ posture }}{% else %}—{% endif %}
|
{% if state.get('was_cached') or meta.get('was_cached') %}
cached
{% if state_cached_from is not none %}
from turn {{ state_cached_from }}
{% endif %}
{% else %}—{% endif %}
|
{{ r.created_at | fmtdate }} |
{% if r.run_id and r.request_id %}
View
{% endif %}
|
{% endfor %}
{% else %}
No turns recorded for this conversation.
{% endif %}
Per-turn detail
{% for r in timeline.requests %}
{% set meta = r.meta_json__parsed or {} %}
{% set state = r.state or {} %}
{% set summary = state.get('state_summary_json__parsed') or {} %}
{% set state_in_full = state.get('state_in_json__parsed') or {} %}
{% set state_out_full = state.get('state_out_json__parsed') or {} %}
{% set proxy = r.proxy_event or {} %}
{% set proxy_meta = proxy.get('metadata_json__parsed') or {} %}
{% set proxy_headers = proxy.get('headers_json__parsed') or {} %}
{% set risk_score = meta.get('risk_score') %}
{% set meta_cached_from = meta.get('cached_from_turn') %}
{% set state_cached_from = state.get('cached_from_turn') %}
Prompt
{{ (r.prompt or '—') }}
Final response
{{ (r.final_response or '—') }}
Governance decision
| Final action | {{ meta.get('final_action') or '—' }} |
| Risk score | {% if risk_score is not none %}{{ "%.4f" | format(risk_score) }}{% else %}—{% endif %} |
| Path | {{ meta.get('path') or '—' }} |
| Domain overlay | {{ meta.get('domain_overlay') or '—' }} |
| Reason codes | {{ (meta.get('reason_codes') or []) | join(', ') or '—' }} |
| Triggered principles | {{ (meta.get('triggered_principles') or []) | join(', ') or '—' }} |
| Posture (final) | {{ meta.get('governance_posture') or summary.get('last_governance_posture') or '—' }} |
| Was cached | {% if meta.get('was_cached') or state.get('was_cached') %}yes{% else %}no{% endif %} |
{% if meta_cached_from is not none or state_cached_from is not none %}
| Cached from turn | {{ meta_cached_from if meta_cached_from is not none else state_cached_from }} |
{% endif %}
{% if meta.get('decision_reason') %}
Decision rationale
{{ meta.get('decision_reason') }}
{% endif %}
{% if meta.get('decision_explanation') %}
Decision explanation (structured)
{{ meta.get('decision_explanation') | tojson(indent=2) }}
{% endif %}
{% if meta %}
Raw governance meta (JSON)
{{ meta | tojson(indent=2) }}
{% endif %}
Conversation state
| Active domain | {{ summary.get('active_domain') or '—' }} |
| Active overlay | {{ summary.get('active_overlay') or '—' }} |
| Hard constraints triggered | {{ (summary.get('last_hard_constraints_triggered') or []) | join(', ') or '—' }} |
| Principle shortlist | {{ (summary.get('principle_shortlist') or []) | join(', ') or '—' }} |
| Refresh required | {% if state.get('refresh_required') %}yes{% else %}no{% endif %} |
{% if state.get('refresh_reason') %}
| Refresh reason | {{ state.get('refresh_reason') }} |
{% endif %}
{% if state_in_full or state_out_full %}
Full state snapshots (JSON)
{% if state_in_full %}
State (in)
{{ state_in_full | tojson(indent=2) }}
{% endif %}
{% if state_out_full %}
State (out)
{{ state_out_full | tojson(indent=2) }}
{% endif %}
{% endif %}
{% if r.ledger_events %}
Ledger activity
| Op | Outcome | Similarity | Reason | From turn | Posture | Final action |
{% for ev in r.ledger_events %}
| {{ ev.operation or '—' }} |
{{ ev.outcome or '—' }} |
{% if ev.similarity is not none %}{{ "%.3f" | format(ev.similarity) }}{% else %}—{% endif %} |
{{ ev.reason or '—' }} |
{{ ev.from_turn if ev.from_turn is not none else '—' }} |
{{ ev.posture or '—' }} |
{{ ev.final_action or '—' }} |
{% endfor %}
{% endif %}
{% if r.session_events %}
Session store activity
| Op | Outcome | Detail |
{% for ev in r.session_events %}
{% set payload = ev.get('payload_json__parsed') or {} %}
{% set state_sum = ev.get('state_summary_json__parsed') or {} %}
{% set ttl_age = payload.get('ttl_age_ms') %}
{% set evicted_ids = payload.get('evicted_ids') %}
| {{ ev.operation or '—' }} |
{{ ev.outcome or '—' }} |
{% if state_sum.get('last_governance_posture') %}posture={{ state_sum.get('last_governance_posture') }}{% endif %}
{% if ttl_age is not none %} · ttl_age={{ ttl_age }}ms{% endif %}
{% if evicted_ids %} · evicted={{ evicted_ids | length }}{% endif %}
|
{% endfor %}
{% endif %}
{% if proxy %}
Proxy finalization
| State provided | {% if proxy.state_provided %}yes{% else %}no{% endif %} |
| State updated | {% if proxy.state_updated %}yes{% else %}no{% endif %} |
| Posture in → out | {{ proxy.posture_in or '—' }} → {{ proxy.posture_out or '—' }} |
| Was cached | {% if proxy.was_cached %}yes{% else %}no{% endif %} |
| Final response length | {{ proxy.final_response_length if proxy.final_response_length is not none else '—' }} |
{% if proxy_headers %}
X-MoralStack headers
{{ proxy_headers | tojson(indent=2) }}
{% endif %}
{% endif %}
{% endfor %}
{% endblock %}