{% extends "base.html" %} {% block title %}Conversation {{ conversation_id[:12] }} - MoralStack{% endblock %} {% block content %} {#- Colour a final_action badge by the strip's legend (green=NORMAL_COMPLETE, amber=SAFE_COMPLETE, red=REFUSE). A pipeline-failure turn persists a coerced NORMAL_COMPLETE placeholder, so it stays neutral (never green) — the adjacent "not a governed outcome" text carries the meaning. -#} {% macro action_badge(action, is_failure=False) -%} {%- if not action -%} — {%- elif is_failure -%} {{ action }} {%- else -%} {{ action }} {%- endif -%} {%- endmacro %}
Multi-turn governance timeline (Step 13 observability)
⚠ turn_index repeats within this conversation{% if timeline.turn_index_collision_multi_run %} — at least one repeat spans separate runs (independent runs sharing this conversation id, not one continuous run){% else %} — the repeat is within a single run (turn_index did not advance across turns){% endif %}. See the #pos/size annotations below for exact grouping.
{% endif %} {% set ov = timeline.overview or {} %} {% set ov_max_risk = ov.get('max_risk_score') %} {% if timeline.pipeline_failure_count %}⚠ {{ timeline.pipeline_failure_count }} turn{{ 's' if timeline.pipeline_failure_count != 1 else '' }} ended in a pipeline failure — its final_action is not a governed outcome.
{% endif %} {% set token_usage_title = 'Token usage by model (conversation)' %} {% include "_token_usage.html" %} {# Conversation-level spine: replaces the horizontal strip. One node per turn (input -> decision -> outcome), a first-turn node for contract/history context, and a terminal node folding the failure-aware aggregates. Overview only — per-turn internals stay on the request page (linked from each node). Order follows seq_pos (turn_index, created_at); parent_request_id is never used for ordering (100% self-referential — see docs/CODEBASE_FACTS.md). #} {% if timeline.requests %}
One node per turn: what it saw (input), what governance decided (decision),
what was delivered (outcome). Risk is the exact decision-trace value plus a
proportional bar (not height — a deliberate substitution, see
.claude/ui-loop/DECISIONS.md). Order follows creation order
(turn_index, created_at); a dashed divider marks turns sharing the same
turn_index, where order is not established — never a causal claim.
“{{ (prompt_preview[:140] + '…') if prompt_preview | length > 140 else prompt_preview }}”
{% endif %} {% endif %}{{ d.winning_rule }}
{% endif %} {% if d.hard_violation_codes %}{% for code in d.hard_violation_codes %} {{ code }} {% endfor %}
{% endif %} {% set causal = none %} {% if d.final_action == 'NORMAL_COMPLETE' %}{% set causal = d.why_not_normal_complete %} {% elif d.final_action == 'SAFE_COMPLETE' %}{% set causal = d.why_not_safe_complete %} {% elif d.final_action == 'REFUSE' %}{% set causal = d.why_not_refuse %} {% endif %} {% if causal %}{{ causal }}
{% endif %} {% elif not r.get('pipeline_failure') %} no risk claim (no FINAL decision row for this turn) {% endif %}“{{ (response_preview[:140] + '…') if response_preview | length > 140 else response_preview }}”
{% endif %} {% if node.outcome.posture %} {{ node.outcome.posture }} {% endif %} {% if node.outcome.was_cached %} {% endif %} {% endif %}| Turn | Request | Action | Risk | Posture (out) | Cached | Created | Actions |
|---|---|---|---|---|---|---|---|
|
{{ r.turn_index if r.turn_index is not none else '—' }}
{% if r.turn_index_collision %}
#{{ r.turn_index_collision_pos }}/{{ r.turn_index_collision_group_size }}{% if r.turn_index_collision_multi_run %} — separate runs share this turn_index{% else %} — same run, turn_index did not advance{% endif %} {% endif %} |
{% if r.run_id and r.request_id %} {{ r.request_id[:12] }} {% else %}{{ (r.request_id or '')[:12] }}{% endif %} |
{{ action_badge(meta.get('final_action'), r.get('pipeline_failure')) }}
{% if r.get('pipeline_failure') %}
not a governed outcome {% endif %} |
{% if risk_score is not none %}
{{ "%.3f" | format(risk_score) }}
{% if r.get('pipeline_failure') %}
fail-closed default{% if r.get('last_assessed_risk') is not none %} (last assessed {{ "%.3f" | format(r.get('last_assessed_risk')) }}){% endif %} {% endif %} {% 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 %} |
No turns recorded for this conversation.
Shares turn_index {{ r.turn_index }} with {{ r.turn_index_collision_group_size - 1 }} other request{{ 's' if r.turn_index_collision_group_size != 2 else '' }} in this conversation ({{ 'separate runs' if r.turn_index_collision_multi_run else 'same run' }}). Position {{ r.turn_index_collision_pos }} of {{ r.turn_index_collision_group_size }} by creation order (row {{ r.seq_pos }} of {{ timeline.requests | length }} overall).
{% endif %}{{ (r.prompt or '—') }}
{{ (r.final_response or '—') }}
| Final action | {{ action_badge(meta.get('final_action'), r.get('pipeline_failure')) }}{% if r.get('pipeline_failure') %} not a governed outcome{% endif %} |
| Risk score | {% if risk_score is not none %}{{ "%.4f" | format(risk_score) }}{% if r.get('pipeline_failure') %} fail-closed default{% if r.get('last_assessed_risk') is not none %} (last assessed {{ "%.4f" | format(r.get('last_assessed_risk')) }}){% endif %}{% endif %}{% 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 %} |
| Cached from turn | {{ meta_cached_from if meta_cached_from is not none else state_cached_from }} |
{{ meta.get('decision_reason') }}
{% endif %}
{% if meta.get('decision_explanation') %}
{{ meta.get('decision_explanation') | tojson(indent=2) }}
{{ meta | tojson(indent=2) }}
| 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 %} |
| Refresh reason | {{ state.get('refresh_reason') }} |
{{ state_in_full | tojson(indent=2) }}
{% endif %}
{% if state_out_full %}
{{ state_out_full | tojson(indent=2) }}
{% endif %}
| Op | Outcome | Similarity | Reason | From turn | Posture | Final action |
|---|---|---|---|---|---|---|
| {{ 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 '—' }} |
| Op | Outcome | Detail |
|---|---|---|
| {{ 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 %} |
| 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 '—' }} |
{{ proxy_headers | tojson(indent=2) }}