feat(events): causal timeline over recorded links only

A bounded walk (six hops, two hundred events) over the links events
actually record: causation ids, parent ids and cited-event evidence,
backwards towards causes and forwards towards consequences, with the edge
kind on every arrow. Nodes carry their cycle for grouping, labelled as
correlation rather than causation, and the artefacts (beliefs, memories,
model calls) they rest on. Cyclic references terminate. Exposed at
GET /api/v1/events/{id}/chain and /events/{id}/timeline, linked from the
event page, and documented.
This commit is contained in:
Andy Dixon
2026-08-22 19:47:22 +01:00
parent 844de93c8b
commit 9eed6e2a18
12 changed files with 491 additions and 1 deletions
+3
View File
@@ -231,3 +231,6 @@ canvas.graph { width: 100%; height: 560px; display: block; border-radius: 8px; b
/* Study: replay & experiments */
.replay-step { padding: .6rem 0; border-top: 1px solid var(--border); } .replay-step:first-child { border-top: 0; } .replay-step ul { margin: .2rem 0 0 1rem; padding: 0; } .strong { font-weight: 600; }
.break { word-break: break-all; }
/* Causal timeline */
.tl-cycle { border-left: 2px solid var(--border); margin: .6rem 0 .6rem .4rem; padding: .2rem 0 .2rem .9rem; } .tl-node { padding: .35rem 0 .35rem .6rem; border-left: 2px solid transparent; } .tl-node.current { border-left-color: var(--accent); background: var(--surface-2); border-radius: 4px; } .tl-when { font-family: var(--mono); }
+1 -1
View File
@@ -8,7 +8,7 @@
<section class="grid cols-3-1">
<div class="stack">
<div class="card">
<h2>Causal chain</h2>
<h2>Causal chain <a class="small" href="/events/{{$e.ID}}/timeline">full timeline →</a></h2>
<div class="chain">
{{if $c.Cause}}<div class="chain-node"><span class="muted small">caused by</span><br><span class="etype cat-{{$c.Cause.Category}}">{{$c.Cause.Type}}</span> <a href="/events/{{$c.Cause.ID}}">{{$c.Cause.Summary}}</a></div><div class="chain-arrow">↓</div>{{end}}
<div class="chain-node current"><span class="etype cat-{{$e.Category}}">{{$e.Type}}</span> {{$e.Summary}}</div>
+19
View File
@@ -0,0 +1,19 @@
{{define "content"}}
{{$o := .Data.Origin}}{{$c := .Data.Chain}}
<div class="pagehead">
<p class="muted small"><a href="/minds/{{.Data.Mind.Slug}}">{{.Data.Mind.DisplayName}}</a> › <a href="/events/{{$o.ID}}">event #{{$o.Seq}}</a> › timeline</p>
<h1>Causal timeline</h1>
<p class="muted">Around <span class="etype cat-{{$o.Category}}">{{$o.Type}}</span> {{$o.Summary}}. Arrows are the links events recorded when they were written: <span class="badge small">caused</span> (causation), <span class="badge small">parent</span> (parent ids) and <span class="badge small">cited</span> (evidence references). Grouping by cycle is correlation, shown for orientation and <strong>not</strong> a causal claim. Nothing here is inferred.</p>
<form method="get" class="row"><label>Direction <select name="direction"><option value="both" {{if eq .Data.Direction "both"}}selected{{end}}>both</option><option value="back" {{if eq .Data.Direction "back"}}selected{{end}}>back (causes)</option><option value="forward" {{if eq .Data.Direction "forward"}}selected{{end}}>forward (consequences)</option></select></label> <label>Depth <input type="number" name="depth" value="{{.Data.Depth}}" min="1" max="6" size="2"></label> <button class="btn small">Redraw</button>{{if $c.Truncated}} <span class="badge warn">truncated at {{len $c.Nodes}} events</span>{{end}}</form>
</div>
<div class="card">
{{range .Data.Groups}}<div class="tl-cycle"><div class="muted small mono">cycle {{if .Cycle}}{{trunc (printf "%s" .Cycle) 8}}{{else}}—{{end}} <span class="muted">(same-cycle grouping; correlation, not causation)</span></div>
{{range .Nodes}}<div class="tl-node {{if eq .Event.ID $o.ID}}current{{end}}">
<div class="tl-when muted small">#{{.Event.Seq}} · {{.Event.OccurredAt.Local.Format "15:04:05.000"}} · {{if lt .Depth 0}}{{.Depth}} (upstream){{else if gt .Depth 0}}+{{.Depth}} (downstream){{else}}origin{{end}}</div>
<div><span class="etype cat-{{.Event.Category}}">{{.Event.Type}}</span> <a href="/events/{{.Event.ID}}">{{.Event.Summary}}</a> <span class="muted small">· {{.Event.Source}}</span></div>
{{with index $.Data.Inbound .Event.ID}}<div class="small muted">{{range .}}← <span class="badge small">{{.Kind}}</span> <a href="/events/{{.From}}">{{trunc (index $.Data.Names .From) 70}}</a> {{end}}</div>{{end}}
{{if .Artefacts}}<div class="small muted">rests on: {{range .Artefacts}}{{if eq .Kind "belief"}}<a href="/minds/{{$.Data.Mind.Slug}}/beliefs/{{.ID}}">belief</a>{{else if eq .Kind "memory"}}<a href="/minds/{{$.Data.Mind.Slug}}/memory/{{.ID}}">memory</a>{{else if eq .Kind "llm_call"}}<a href="/llm/calls/{{.ID}}">model call</a>{{else}}{{.Kind}}{{end}}{{with .Note}} ({{.}}){{end}} · {{end}}</div>{{end}}
</div>{{end}}
</div>{{end}}
</div>
{{end}}