I have been building a tool called TORQUE — a manual analysis pipeline for measuring drift in AI-assisted conversations. The pipeline produces a lot of structured data per session: per-turn drift markers, state snapshots, concept lineage, pivot points. The visualization is a React dashboard with six charts. The interesting engineering problem turned out not to be the charts. It was how to update a dashboard inside an artifact without paying token rent on every refresh.
The naive shape is one artifact: chart code plus a data object, regenerated every time the data changes. For a thirty-turn session that is roughly 15KB of rendering logic plus 8KB of data, regenerated maybe ten times across the analysis. Two hundred and thirty kilobytes of redundant output for code that did not change. It also means every refresh contains the chart definitions and could subtly drift if the agent decides to "improve" them mid-session — which, given that the agent under analysis is the same agent producing the artifact, is exactly the failure mode I cannot afford.
Two artifacts, one job each
The shape I landed on splits the work in two:
- The dashboard. Rendering logic only. Reads from
window.storageon mount. Generated once. Never rewritten unless the chart logic itself needs to change. - The data push. Data only. On mount, writes the current
TORQUE_DATAobject towindow.storage, then renders a small confirmation card showing session stats. Regenerated on every refresh — this is the only output token cost per update.
The dashboard is roughly 15KB and stable. The data push is 3–18KB depending on session size. The math falls out: a thirty-turn session gets ten refreshes for maybe 80KB of total output instead of 230KB, and the dashboard stops drifting because it is not being rewritten.
How the handoff works
The persistent storage available to artifacts is just a key-value store with a few rules: keys under 200 chars, values under 5MB, last-write-wins on concurrent updates. The dashboard's mount logic is small enough to quote in full:
useEffect(() => {
(async () => {
try {
const result = await window.storage.get("torque-session");
if (!result) { setEmpty(true); return; }
setData(JSON.parse(result.value));
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
})();
}, []);
The data push is even shorter — it stringifies a constant object and calls window.storage.set. The user opens the data push artifact, which writes; then opens or refreshes the dashboard, which reads. Both can stay open in different panes; the dashboard just needs a re-mount to pick up the new data.
The token math, more carefully
Per-refresh cost is the size of the JSON payload plus a thin React wrapper — call it 200 bytes of overhead. For a session with sixty turns and twelve traced concepts, the data object lands at about 14KB. Regenerating the entire dashboard alongside that data would push the per-refresh cost to roughly 30KB. Across a long analysis with twenty refreshes, that is the difference between about 280KB and about 600KB of output. Not catastrophic — but the point is not the savings. The point is that the dashboard's behavior stops being a function of how many times the agent has rewritten it.
What you give up
Two things, both worth naming:
- You cannot change the schema and the renderer in a single move. If the data push starts emitting a new field, the dashboard either has to handle it gracefully (defensive defaults) or be re-issued in the same turn. I treat the schema as the contract and rev it deliberately.
- The user has to open the data push. It does not run in the background. The artifact has to render at least once for its
useEffectto fire and write the value. Fine in practice, but it is a real step in the loop, not magic.
The context window is the real ceiling
All of the analysis state — every drift marker, every concept trace, every state snapshot — lives in the agent's context window during a session. The data push is just a serialization of that state at a moment in time. When the conversation gets long enough that the agent's recall starts to degrade, the right move is to dump the data object out of the data push, paste it into a new chat, and continue from there. Persistent storage is a bridge between artifacts; it is not a memory of the analysis. The memory is still in the conversation, and it has a ceiling.
I added an alert to the agent's behavior spec for this: when the in-context state is approaching a size where recall will start to fail, surface it before the next refresh. Better to break the analysis into a second session at a known boundary than to discover later that the third pivot point was misclassified because the agent had forgotten the first one.
When this pattern is worth it
The split is useful any time you have a long-lived visualization that consumes accumulating structured data over a single session. Diagnostic dashboards, scoring trackers, build status walls, anything where the chart code is stable and the numbers move. If your visualization is one-shot, the split is overkill — generate one artifact and move on. If it will be refreshed twice, you are right at breakeven. From three on, the split pays.
The dashboard's job is to render. The data push's job is to deliver. Once those two jobs stop overlapping, the artifact stops costing what it should not.