The previous post introduced three AI-facing surfaces — conversation-arc telemetry, annotation provenance + lifecycle, variant discovery — as M1 deliverables. M1 was the spec: types, contracts, stub implementations you could wire end-to-end but whose behavior was still mostly text.
This post is what shipped between then and now to make them real. The headline change isn't one feature; it's that the three surfaces now feel like one system instead of three scaffolds that happen to live in the same subpath.
The annotations age
The lifecycle surface ships computeAnnotationFreshness andapplyAnnotationLifecycle as runnable code. Pass an array of annotations plus a "now" reference (or a chart's data extent, for streaming) and the helper classifies each into a band, applies a default visual treatment, and filters expired entries:
import { applyAnnotationLifecycle, withCurrentProvenance } from "semiotic/ai" const ann = withCurrentProvenance( { type: "callout", t: latest.t, value: latest.value, label: "Spike", lifecycle: { ttlHint: 3000 } }, { author: "alice", source: "user" } ) <LineChart data={streaming} xAccessor="t" yAccessor="value" annotations={applyAnnotationLifecycle([ann], { dataExtent: [streaming[0].t, streaming.at(-1).t], })} />The default treatment is opinionated: aging dims to 0.55 opacity, stale drops to 0.35 and adds a dashed border viastrokeDasharray="4 4" (the renderer now honors the attribute and cascades it through the annotation's stroked children), andexpired is filtered from the array unlessshowExpiredAnnotations: true. Each band's treatment is overridable via the options object, with explicit fields on the annotation winning over the treatment.
See it in action on the/intelligence/conversation-arc lifecycle scrubber, or the streaming variant on/intelligence/temporal-lifecycle — the second one shows a streaming chart where the annotation ages against chart-time (the latest data point's timestamp), not wall-clock. Pause the stream and the annotation stops aging.
The arc reacts
The conversation-arc store kept its module-scope shape — same record/subscribe/flush API — but grew a React hook, useConversationArc, that owns subscription, snapshot stability, and re-render coordination. The hook returns a livehistory array, a reducedsummary object (per-type counts, components seen, audiences seen, duration), and a stable record callback. Two hook instances on the same page see the same buffer.
function ArcInspector() { const { history, summary, record } = useConversationArc() return ( <> <header> {summary.total} events · {summary.byType["chart-exported"] ?? 0} exports · components: {summary.componentsSeen.join(", ")} </header> <EventList events={history} /> </> ) }The more interesting move is one layer up: two of the existing AI hooks now auto-emit arc events. useChartSuggestions emitssuggestion-shown whenever the ranked list changes (deduplicated by component-list signature so React's strict-mode double-render doesn't double-stamp). useChartInterrogation emitsinterrogation-asked on ask()and interrogation-answered on response — with measuredlatencyMs fromperformance.now().
Try it. The demo below renders intent buttons and an audience picker. The component code makes norecord() calls of its own — onlyuseChartSuggestions running andrecordAudienceChange on the picker. Every event in the panel on the right came from framework-level instrumentation:
Audience picker (recordAudienceChange)
Top suggestion
BarChartscore 1.0also:DotPlot, PieChart, semiotic.recipe.waffle.v0
0 events buffered · 0 suggestions · 0 audience changes
Latest arc events (no record() calls in this component)
Change intent or audience to fire events.
The point isn't that auto-instrumentation is novel. The point is that the arc captures itself. A consumer who never reads the conversation-arc docs at all still produces a recoverable session as soon as they callenableConversationArc() — even if their app is justuseChartSuggestions and a chart. That's the talk's claim about "data nobody else is collecting" becoming actually true rather than aspirational.
Three policies, one classifier
Semiotic had three "how does this thing look as it ages?" systems already:DecayConfig (continuous opacity ramp by buffer position),StalenessConfig (binary live/stale by wall-clock idle), and now annotation freshness (four named bands by TTL). Three policies on three different time axes, all answering the same shape of question.
They now share one classifier:bandFromAge(ageMs, ttlMs, thresholds?) — a pure function exported from both semiotic/realtime andsemiotic/ai. Annotation freshness uses it today; the other two systems can opt in when a binary or continuous policy doesn't fit. The shipped default thresholds (1× / 1.5× / 3× TTL) live next to the function asDEFAULT_LIFECYCLE_THRESHOLDS, so downstream code that needs to introspect or override them has one place to read.
The same unification round also de-duped AnnotationAnchor. It used to be defined twice — once in realtime/types.ts asAnnotationAnchorMode, once inai/annotationProvenance.ts asAnnotationAnchor. Same three modes (fixed / latest / sticky); the AI side had added a fourth (semantic) on top. The canonical type now lives in the realtime runtime (next to itsstickyPositionCache implementation) with the AI surface re-exporting and the new semantic mode folded in.AnnotationAnchorMode is a@deprecated alias for the old name.
Full survey at/intelligence/temporal-lifecycle — that page also has the interactive band-from-age scrubber and a streaming chart whose annotation ages live.
What's still in the queue
Variant discovery's M1 ships the type contract and a working registration plug point —proposeVariant now dispatches through every registered proposer and deduplicates by proposal id. The heuristic reference proposer that walks existing variants and flips orientation / toggles normalizeis still M2 work. Same goes for the variant scorer and the MCPproposeChartVariants tool.
On the conversation-arc side, the store now has first-party persistence hooks:registerConversationArcSink,createLocalStorageConversationArcSink,createIndexedDBConversationArcSink, andcreateWebhookConversationArcSink. Replay stays separate from recording through loadConversationArc, so a fixture can hydrate the inspector without duplicating analytics events.
The annotation side now gives the semantic anchor an actual resolution algorithm — finding "the Q3 spike" by stableIdafter the data refreshes, then falling back to the recorded coordinate when the target is gone. That's the M3 hook into the existingannotationResolvers.ts pathway, which is why anchor mode dedup mattered for unblocking it.
What this looks like from the outside
A consumer that adopts a single line —enableConversationArc() at app startup — now gets, for free:
- Every chart-suggestion ranking that the library showed.
- Every interrogation round-trip the user did, with measured latency.
- Every audience change, if they call
recordAudienceChangein their picker. - A flushable buffer they can send anywhere.
- Annotations on their charts that visibly age over time, with a treatment they didn't have to design.
That's the surface that wasn't there a month ago. The talk's claim about "library-collected session data" stops being a promise and starts being a thing pointed at on the screen.