diff --git a/content/blog/2026-06-27-two-memories.md b/content/blog/2026-06-27-two-memories.md
new file mode 100644
index 0000000..32798b4
--- /dev/null
+++ b/content/blog/2026-06-27-two-memories.md
@@ -0,0 +1,228 @@
++++
+title = "Two Memories: Honcho vs Hindsight, all the way down"
+date = 2026-06-27
+[taxonomies]
+tags = ["agent-memory", "llm", "honcho", "hindsight", "systems"]
+categories = ["engineering"]
++++
+
+
+
+If you ask an LLM to compare [Honcho](https://github.com/plastic-labs/honcho) and [Hindsight](https://github.com/vectorize-io/hindsight), you get mush. Both "give your agent long-term memory." Both "go beyond RAG." Both "reason." The mush isn't the model's fault — it's downstream of docs that are themselves vague, and a marketing layer that wants every system to sound like the same magic.
+
+So I cloned both repos and read down to the schema. The two systems are **not** two implementations of one idea. They're two opposing bets about *where intelligence should live in a memory system* — and that single disagreement propagates cleanly from design philosophy all the way down to their `CREATE TABLE` statements. One puts the intelligence in a model. The other puts it in the structure. Everything else follows.
+
+What follows is a descent. Each layer is the same question asked further down: how does the system decide what's true, what to keep, what to surface, and how to store it. Amber is Honcho; teal is Hindsight.
+
+## Layer 00 — The split
+
+Honcho (Plastic Labs) models **people**. Its thesis is "memory as reasoning": it treats remembering as a logical-reasoning task and builds a theory-of-mind representation of each participant. Tellingly, *both* users and agents are "peers" — it's fundamentally a social-cognition engine.
+
+Hindsight (Vectorize, with Virginia Tech and The Washington Post) models **the world and the agent's own experience**, with a hard wall between *evidence* and *inference*. The architecture exists to keep "what the agent saw" separate from "what the agent believes," over a queryable timeline.
+
+> One bets on weights. The other bets on structure. Read every layer below as that same wager, restated.
+
+## Layer 01 — The reasoning engine
+
+This is the cleanest expression of the split, and the first place the docs mislead.
+
+Honcho trains a model. **Neuromancer XR** is a fine-tune of Qwen3-8B on a hand-curated dataset (~10k social-reasoning traces) mapping conversation turns to atomic conclusions along a certainty spectrum. Crucially it's trained *once*, by Plastic Labs, ahead of time. Your conversations run *inference* through frozen weights — no gradients touch your data. The "continual learning" is rows accumulating in Postgres, not weight updates. (Swap Neuromancer for GLM-5 or Grok and you keep all your memory — proof it lives in the DB, not the model.)
+
+Hindsight trains nothing. Its reasoning layer, CARA, is a prompt-and-orchestration framework over *any* model you configure. It's LLM-based at several stages — fact extraction, a cross-encoder reranker, response generation — all swappable modules. The intelligence is in the pipeline and the prompts, not a specialized weight set.
+
+
+
Fig 1 · published benchmarks Honcho's win comes from a small specialized model; Hindsight's from a generic backbone inside a structured pipeline. These are the numbers each project reports — different benchmarks, not a head-to-head — so read them as "what each bet buys," not a leaderboard.
+
+So the privacy story is the ordinary one (your text sits in their Postgres unless you self-host), not "my data is in a model." And the engineering story is the deep one: **Honcho put a brain in the box; Hindsight built a very good filing system and rents a brain by the call.**
+
+## Layer 02 — Representation
+
+What is a single "memory"? They answer in opposite directions.
+
+Honcho: atomic conclusions. A memory is an *atomic conclusion* about a peer, placed on a strict spectrum of logical certainty — `explicit` (stated), `deductive` (necessarily follows), `inductive` (likely pattern), `abductive` (best explanation). Conclusions are recomposable and link to the premises they came from.
+
+Hindsight: narrative facts. A memory is a *coarse narrative fact* (2–5 per conversation, each covering a whole exchange), filed into one of four networks by epistemic role — `world`, `experience`, `opinion`, `observation`.
+
+Note the inversion: Honcho makes facts **atomic and recomposable** so a reasoner can scaffold them; Hindsight keeps them **chunky and narrative** so retrieval is robust to where you split the conversation. And neither stores its "types" as separate tables — both use a single fact table with a discriminator column. The four networks are a `WHERE fact_type = …`, not four databases (more at Layer 07).
+
+## Layer 03 — Retrieval
+
+Honcho retrieves with an agent. The Dialectic is the one tool-using agent on the synchronous path; it loops over its tools until it has enough context, with a tunable reasoning budget (`minimal → max`, priced per query). Retrieval is a decision the model makes.
+
+Hindsight retrieves with a fixed pipeline. TEMPR runs four retrievers in parallel — semantic vector, BM25, graph, temporal — fuses them with Reciprocal Rank Fusion, reranks with a cross-encoder, trims to a token budget. No agent decides; the arithmetic does.
+
+
+
Fig 2 · the two recall paths left, an adaptive tool loop; right, a deterministic four-arm fusion. Adaptive vs predictable — the same trade reappears in the ranking math at Layer 06.
+
+The RRF core is textbook, constant and all:
+
+```python
+# hindsight · search/fusion.py — four arms, fixed priority
+def reciprocal_rank_fusion(result_lists, k=60):
+ # score(d) = Σ_arms 1 / (k + rank(d))
+ ...
+```
+
+But right below it sits `interleave_fusion` — an admission against the tidy formula. RRF *sums* reciprocal ranks, so a near-duplicate that's rank #1 in *one* arm but absent from the others gets averaged down and dropped below the budget — the exact failure that makes consolidation create duplicates. Their fix is a round-robin that guarantees every arm's top hit a slot. The elegant formalism had a hole; they patched it with an inelegant interleave. You only find that in the source.
+
+## Layer 04 — Belief and contradiction
+
+When new information contradicts old, the systems disagree about what "confidence" even is.
+
+Hindsight is numeric. An opinion is a tuple `(text, c, τ)` with `c ∈ [0,1]`. New evidence is classified and the scalar is nudged:
+
+```text
+c' = min(c + α, 1) # reinforce
+c' = max(c − α, 0) # weaken
+c' = max(c − 2α, 0) # contradict ← costs double
+c' = c # neutral
+```
+
+That `2α` is the entire deconfliction philosophy in one coefficient: damped but responsive, so beliefs don't oscillate on a single example. And the evidence/inference wall is enforced *in the database* — the CHECK constraint at Layer 07.
+
+Honcho refuses numbers. It deliberately avoids "arbitrary numerical tokens for certainty," using natural-language tiers plus reasoning traces. Contradiction is handled by re-reasoning: the deduction specialist treats a changed fact as a knowledge update, writes a dated update observation with explicit premises, and **deletes the stale one**. Reconciliation is generative and offline, not arithmetic and online.
+
+A quiet convergence worth noting: both end up *deleting* superseded beliefs — Honcho via a soft-delete in its deduction agent, Hindsight via an LLM-driven move to a tombstone table. They reach "prune the past" from opposite directions: one through agent judgment, one through a consolidation engine.
+
+## Layer 05 — The dreamer, and surprisal
+
+This is the find that most repays reading the source. Honcho's docs mention "reasoning trees," so I expected a logic graph in `src/dreamer/trees/`. It isn't. It's a set of spatial nearest-neighbor structures — random-projection trees, cover trees, LSH — whose only job is to compute **geometric surprisal** over observation embeddings.
+
+Before the dreamer spends a single expensive reasoning call, it decides *what's worth thinking about* using information theory. It builds a tree over the cloud of a peer's existing observations and scores each by how improbable its path through that tree is:
+
+```text
+# honcho · dreamer/trees/rptree.py
+S(x) = −log P(path to x) = Σ −log( n_child / n_parent )
+```
+
+An observation landing in a dense, well-trodden region has a high-probability path → *low* surprisal → redundant. One that forces traversal down sparse branches → *high* surprisal → novel or anomalous. The pipeline keeps only the top slice and feeds *those* to the deductive / inductive / abductive specialists.
+
+
+
+
+
+
+
+
+
Fig 3 · surprisal, illustrated Each drop lands an observation in embedding space. Near the crowd → low surprisal (dim, skipped). Out in empty space → high surprisal (bright, sent to the reasoners). This is the throttle that keeps "dreaming" affordable: spend scarce reasoning only on the surprising tail.
+
+So Honcho's most sophisticated code is a mechanism for *not* reasoning about most things. That tells you exactly what it's bound by: **reasoning compute**. The whole architecture rations an expensive, scarce resource and spends it only where novelty says it'll pay off. (The actual premise-linked "reasoning tree," by the way, lives elsewhere — it's the mandatory `source_ids` linkage the deduction agent writes on every conclusion. Two different "trees" sharing a module; no wonder the docs confuse.)
+
+## Layer 06 — The recency–relevance arbitration
+
+The very bottom of retrieval: in a tie, do you get the recent answer or the relevant one? Here the split is almost too clean.
+
+Honcho has no recency formula at all. Grep the whole repo for `recency_boost`, `half_life`, `time_decay` — zero hits. Instead it exposes three separate primitives as tools and lets the Dialectic agent *choose*: cosine-only semantic, `created_at`-only recent, or `times_derived`-then-recency for reinforced observations. Recency-vs-relevance is an agent decision, not a blend.
+
+Hindsight has a closed-form score. Relevance is the multiplicative base; recency, temporal proximity, and proof-count are *bounded* modifiers on top:
+
+```python
+# hindsight · search/reranking.py
+combined = CE_norm * recency_boost * temporal_boost * proof_count_boost
+recency_boost = 1 + alpha * (recency - 0.5) # alpha=0.2 → range [0.9, 1.1]
+# combined envelope: max ≈ +21%, min ≈ −19% → recency can NEVER trump relevance
+```
+
+Drag `α` below and watch the January "I love Python" fact and the June "Rust won me over" fact resolve. Recency is a tiebreaker, not a trump — and there's an explicit guardrail so that when the cross-encoder gives no signal, the order doesn't collapse into a pure recency sort.
+
+
+
+
+
+
+
+ α = 0.20
+
+
+ 0.00
+
+
+
+
+
Fig 4 · recency as a bounded modifier The shaded band is the ±envelope recency can move a result. With relevance tied, June wins by the recency margin — repeatably, every run. Give January a big enough relevance edge and recency can't flip it. That determinism is also why this function is trivially unit-testable; an agent tool-loop is not.
+
+## The trace — one fact, both pipelines
+
+Concretely: the user says *"I love Python"* in January, then *"honestly, Rust has won me over"* in June. Watch the same contradiction move through both systems.
+
+
+
+
+
+
+ step 0 / 6
+
+
+
Honchosurprisal → re-reason → delete
+
Hindsightbitemporal retrieve + decay
+
+
+
+The deepest practical difference surfaces here: what each does with the *past*. Honcho prunes the stale "loves Python" observation in favor of a current-truth update — great for "what's true now," weaker for "what did they prefer in February?" Hindsight keeps the January fact as valid history with bitemporal timestamps — heavier, but it answers point-in-time and "how did this belief evolve" queries natively.
+
+## Layer 07 — Storage: the schema is the architecture
+
+The DDL confirms everything above. Count the storage systems first: Hindsight is one database doing everything; Honcho is two (optionally three).
+
+**Honcho — ~11 tables.** PostgreSQL (relational + pgvector HNSW + `tsvector` FTS + a JSONB graph + a `queue` table) plus Redis as a hot cache, plus an optional external vector store (Turbopuffer / LanceDB). Plus a separate deriver worker process.
+
+**Hindsight — ~19 tables.** PostgreSQL *only*: pluggable ANN (pgvector / pgvectorscale-DiskANN / vchord / scann), BM25 via `pgroonga`/`pg_search`, the graph as real edge tables, queues as tables. There's even an in-process embedded mode. No Redis.
+
+Three physical divergences tell the whole story.
+
+**The graph.** Hindsight normalizes it: `memory_links(from_unit_id, to_unit_id, link_type, weight)` is a real edge table, plus `entities`, `unit_entities`, `entity_cooccurrences`. Honcho denormalizes it into a single JSONB column (`source_ids`) with a GIN index — the graph lives *inside* the fact rows.
+
+**Confidence is a database constraint.** Hindsight's evidence/inference firewall isn't convention; it's DDL. A world or experience fact is *forbidden* a confidence score; an opinion is *required* one:
+
+```sql
+-- hindsight · alembic/…initial_schema.py
+CHECK (
+ (fact_type = 'opinion' AND confidence_score IS NOT NULL) OR
+ (fact_type = 'observation') OR
+ (fact_type NOT IN ('opinion','observation') AND confidence_score IS NULL)
+)
+```
+
+Honcho has no confidence column at all — consistent with its "no numeric certainty" stance. The `level` enum *is* the certainty representation.
+
+**History.** Hindsight keeps append-only provenance — `observation_history`, `observation_sources`, `invalidated_memory_units`, `audit_log` — built for "how did this evolve" and audit. Honcho compresses all of it into a `times_derived` counter, a `source_ids` array, and a `deleted_at` soft-delete flag.
+
+The operational corollary: Honcho's footprint is Postgres + Redis + a long-running deriver worker + the model calls. Hindsight is a single Postgres you can even embed in-process. On a RAM-constrained box, that asymmetry can matter more than any algorithm above it.
+
+## The same bet, at every layer
+
+| layer | Honcho | Hindsight |
+|---|---|---|
+| philosophy | models people · reasoning-centric | models world + experience · structure-centric |
+| engine | Neuromancer XR — fine-tuned Qwen3-8B | CARA — generic swappable LLMs + prompts |
+| unit | atomic conclusion · explicit→abductive | narrative fact · world/exp/opinion/obs |
+| retrieval | Dialectic agent loop · budgeted | 4-arm fusion · RRF k=60 + rerank |
+| certainty | linguistic tiers · no number | scalar c∈[0,1] · CHECK-enforced |
+| contradiction | re-reason offline + delete | c−2α + LLM supersede + tombstone |
+| consolidation | surprisal-gated dreaming | proof-count + history tables |
+| ranking | no formula · agent picks tool | CE × recency × temporal × proof |
+| graph | JSONB adjacency + GIN | normalized edge + entity tables |
+| storage | Postgres + Redis (+ ext vec) | Postgres only (or embedded) |
+| license | AGPL-3.0 | MIT |
+
+## Verdict
+
+Neither is "better." They optimize different axes, and your use case picks for you.
+
+Reach for Honcho when the job is **knowing who someone is right now** and reasoning well about it — personalization, theory-of-mind, an agent that should anticipate needs. Its bias toward current-truth-plus-provenance fits, and the surprisal gate keeps dream cost bounded. The cost: a heavier runtime and ranking behavior you can't pin down with a deterministic test.
+
+Reach for Hindsight when you need an **auditable timeline** — "what did this customer believe in Q1 vs Q3," explainable recall, mission-critical recall over many sessions. Bitemporal facts + an evidence/inference firewall + a deterministic, unit-testable ranking function are the win. The cost: a heavier retrieval stack you have to tune, over generic models.
+
+> They're not two takes on one idea. They're two answers to one question: put the intelligence in the model, or put it in the structure. Everything from the philosophy to the `CHECK` constraint is that single choice, restated.
+
+---
+
+*Written from source — every claim traces to a file, a migration, or a paragraph in the paper, not a docs page. Primary sources: [plastic-labs/honcho](https://github.com/plastic-labs/honcho) (AGPL-3.0), [vectorize-io/hindsight](https://github.com/vectorize-io/hindsight) (MIT), and [arXiv:2512.12818](https://arxiv.org/abs/2512.12818), "Hindsight is 20/20," Latimer et al.*
+
+
diff --git a/static/blog/two-memories/widgets.css b/static/blog/two-memories/widgets.css
new file mode 100644
index 0000000..dc34300
--- /dev/null
+++ b/static/blog/two-memories/widgets.css
@@ -0,0 +1,93 @@
+/* ============================================================
+ two-memories — scoped interactive figure styles
+ Everything is namespaced under .tm-fig / #tm-* so nothing
+ leaks into the site's global stylesheet. Figures render as
+ dark "instrument" panels inside the light article.
+ ============================================================ */
+.tm-fig{
+ --tm-honcho:#F2B05E;
+ --tm-honcho-dim:#7a5a32;
+ --tm-hindsight:#4FD0C9;
+ --tm-hindsight-dim:#2c6864;
+ --tm-ink:#11151C;
+ --tm-panel:#171D28;
+ --tm-line:#2A323F;
+ --tm-text:#E9E6DC;
+ --tm-muted:#9AA3B2;
+ --tm-faint:#6B7384;
+ background:linear-gradient(180deg,var(--tm-panel),var(--tm-ink));
+ border:1px solid var(--tm-line);
+ border-radius:14px;
+ padding:22px;
+ margin:30px 0 12px;
+ color:var(--tm-text);
+ font-family:'IBM Plex Mono',ui-monospace,monospace;
+}
+.tm-cap{
+ font-family:'IBM Plex Mono',ui-monospace,monospace;
+ font-size:13px;line-height:1.55;color:#6a6a6a;margin:0 0 26px;
+}
+.tm-cap b{color:#222;font-weight:700;text-transform:uppercase;letter-spacing:.04em;font-size:11.5px}
+.tm-fig .tm-legend{display:flex;gap:20px;flex-wrap:wrap;font-size:12px;color:var(--tm-muted);margin:0 0 16px}
+.tm-fig .tm-legend span{display:inline-flex;align-items:center;gap:8px}
+.tm-dot{width:10px;height:10px;border-radius:50%;display:inline-block}
+.tm-dot.h{background:var(--tm-honcho);box-shadow:0 0 12px var(--tm-honcho)}
+.tm-dot.s{background:var(--tm-hindsight);box-shadow:0 0 12px var(--tm-hindsight)}
+.tm-fig svg{display:block;width:100%;height:auto}
+.tm-fig svg text{font-family:'IBM Plex Mono',ui-monospace,monospace}
+
+/* buttons */
+.tm-btn{
+ font-family:'IBM Plex Sans',system-ui,sans-serif;font-size:13.5px;font-weight:700;
+ background:#1C2330;color:var(--tm-text);border:1px solid var(--tm-line);border-radius:9px;
+ padding:9px 16px;cursor:pointer;transition:.16s}
+.tm-btn:hover{border-color:var(--tm-muted);transform:translateY(-1px)}
+.tm-btn.pri{background:var(--tm-honcho);color:#1a130a;border-color:var(--tm-honcho)}
+.tm-btn:disabled{opacity:.4;cursor:default;transform:none}
+.tm-controls{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:16px;justify-content:center}
+
+/* recency lab */
+.tm-recency{display:grid;grid-template-columns:1fr 1fr;gap:24px;align-items:center}
+@media(max-width:620px){.tm-recency{grid-template-columns:1fr}}
+.tm-ctrl{display:flex;align-items:center;gap:12px;margin:14px 0;font-size:13px;color:var(--tm-muted)}
+.tm-ctrl input[type=range]{-webkit-appearance:none;appearance:none;height:4px;border-radius:3px;
+ background:var(--tm-line);outline:none;flex:1}
+.tm-ctrl input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;border-radius:50%;
+ background:var(--tm-hindsight);cursor:pointer;box-shadow:0 0 12px var(--tm-hindsight-dim)}
+.tm-ctrl input[type=range]::-moz-range-thumb{width:18px;height:18px;border:0;border-radius:50%;
+ background:var(--tm-hindsight);cursor:pointer}
+.tm-read{font-size:13px;color:var(--tm-text)}
+.tm-read b{color:var(--tm-hindsight)}
+.tm-verdict{margin-top:16px;border:1px solid var(--tm-line);border-radius:11px;padding:14px 16px;
+ font-size:12.5px;line-height:1.7;background:var(--tm-ink)}
+.tm-verdict .win{color:var(--tm-hindsight);font-weight:700}
+
+/* trace */
+.tm-trace-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:18px}
+@media(max-width:620px){.tm-trace-grid{grid-template-columns:1fr}}
+.tm-col{border:1px solid var(--tm-line);border-radius:12px;background:#141A24;overflow:hidden}
+.tm-col>h4{margin:0;padding:13px 16px;border-bottom:1px solid var(--tm-line);
+ font-family:'IBM Plex Sans',sans-serif;font-weight:700;font-size:14.5px;
+ display:flex;justify-content:space-between;align-items:center}
+.tm-col.h>h4{color:var(--tm-honcho)}
+.tm-col.s>h4{color:var(--tm-hindsight)}
+.tm-col>h4 .mech{font-size:10px;letter-spacing:.06em;color:var(--tm-faint);text-transform:uppercase;font-weight:400}
+.tm-steps{padding:8px 0}
+.tm-step{padding:12px 16px;border-left:2px solid transparent;opacity:.3;transition:.4s;display:flex;gap:12px}
+.tm-step.on{opacity:1;background:rgba(255,255,255,.025)}
+.tm-col.h .tm-step.on{border-left-color:var(--tm-honcho)}
+.tm-col.s .tm-step.on{border-left-color:var(--tm-hindsight)}
+.tm-step .i{font-size:11px;color:var(--tm-faint);padding-top:2px;min-width:20px}
+.tm-step .x{font-size:14px;line-height:1.5;color:#cfcabb;font-family:'IBM Plex Sans',sans-serif}
+.tm-step .x b{font-family:'IBM Plex Mono',monospace;font-size:12px;font-weight:500}
+.tm-col.h .tm-step .x b{color:var(--tm-honcho)}
+.tm-col.s .tm-step .x b{color:var(--tm-hindsight)}
+.tm-stepn{font-size:12.5px;color:var(--tm-muted)}
+
+@media(prefers-reduced-motion:reduce){
+ .tm-fig *{animation:none!important;transition:none!important}
+}
+
+/* inline prose emphasis — legible on the light article background */
+.tm-h{color:#B5710A;font-weight:600}
+.tm-s{color:#0E8C84;font-weight:600}
diff --git a/static/blog/two-memories/widgets.js b/static/blog/two-memories/widgets.js
new file mode 100644
index 0000000..4a4308a
--- /dev/null
+++ b/static/blog/two-memories/widgets.js
@@ -0,0 +1,200 @@
+/* ============================================================
+ two-memories — interactive figures (vanilla, no deps)
+ Progressive enhancement: if this fails to load, the prose and
+ fenced code in the post still convey everything. All IDs are
+ namespaced tm-*. Respects prefers-reduced-motion.
+ ============================================================ */
+(function () {
+ "use strict";
+ var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ var NS = "http://www.w3.org/2000/svg";
+ function el(t, a) { var e = document.createElementNS(NS, t); a = a || {}; for (var k in a) e.setAttribute(k, a[k]); return e; }
+ function byId(id) { return document.getElementById(id); }
+
+ var HONCHO = "#F2B05E", HONCHO_DIM = "#7a5a32", HIND = "#4FD0C9";
+
+ /* ---------- benchmark bars ---------- */
+ (function bench() {
+ var host = byId("tm-bench"); if (!host) return;
+ var data = [
+ { label: "Neuromancer XR · LoCoMo", val: 86.9, c: HONCHO, note: "vs 80.0 Claude 4 Sonnet baseline" },
+ { label: "base Qwen3-8B · LoCoMo", val: 69.6, c: HONCHO_DIM, note: "the un-tuned backbone" },
+ { label: "Hindsight 20B · LongMemEval", val: 83.6, c: HIND, note: "up from 39.0 full-context, same backbone" },
+ { label: "Hindsight (large) · LongMemEval", val: 91.4, c: HIND, note: "scaled backbone" },
+ { label: "Hindsight (large) · LoCoMo", val: 89.6, c: HIND, note: "vs 75.8 strongest prior open system" }
+ ];
+ var W = Math.min(host.offsetWidth || 640, 760), rowH = 58, pad = 8, max = 100;
+ var svg = el("svg", { viewBox: "0 0 " + W + " " + (data.length * rowH + pad), width: "100%" });
+ data.forEach(function (d, i) {
+ var y = i * rowH + pad, trackY = y + 24, bw = W;
+ var lab = el("text", { x: 0, y: y + 15, fill: "#cfcabb", "font-size": 12.5 }); lab.textContent = d.label; svg.appendChild(lab);
+ svg.appendChild(el("rect", { x: 0, y: trackY, width: bw, height: 14, rx: 7, fill: "#1c2330" }));
+ var bar = el("rect", { x: 0, y: trackY, width: 0, height: 14, rx: 7, fill: d.c }); svg.appendChild(bar);
+ var note = el("text", { x: 6, y: trackY + 44, fill: "#6B7384", "font-size": 11 }); note.textContent = d.note; svg.appendChild(note);
+ var num = el("text", { x: 0, y: trackY + 11, fill: "#0f1219", "font-size": 11, "font-weight": 700, "text-anchor": "end" }); svg.appendChild(num);
+ var target = bw * d.val / max;
+ var obs = new IntersectionObserver(function (e) {
+ if (e[0].isIntersecting) {
+ var t0 = null, dur = 900;
+ (function anim(ts) {
+ if (!t0) t0 = ts; var p = reduce ? 1 : Math.min((ts - t0) / dur, 1), e2 = 1 - Math.pow(1 - p, 3);
+ bar.setAttribute("width", target * e2); num.setAttribute("x", Math.max(target * e2 - 6, 26));
+ num.textContent = (d.val * e2).toFixed(1); if (p < 1) requestAnimationFrame(anim);
+ })();
+ obs.disconnect();
+ }
+ }, { threshold: 0.35 });
+ obs.observe(host);
+ });
+ host.appendChild(svg);
+ })();
+
+ /* ---------- retrieval diagram ---------- */
+ (function ret() {
+ var host = byId("tm-ret"); if (!host) return;
+ var W = Math.min(host.offsetWidth || 640, 760), H = 300;
+ var svg = el("svg", { viewBox: "0 0 " + W + " " + H, width: "100%" });
+ var midX = W / 2 - 14;
+ function lbl(x, y, t, c, sz, anchor, weight) { var e = el("text", { x: x, y: y, fill: c, "font-size": sz || 11, "text-anchor": anchor || "middle", "font-weight": weight || 500 }); e.textContent = t; svg.appendChild(e); }
+ function box(x, y, w, h, c, t) { svg.appendChild(el("rect", { x: x, y: y, width: w, height: h, rx: 8, fill: "none", stroke: c, "stroke-width": 1.3, opacity: .9 })); lbl(x + w / 2, y + h / 2 + 4, t, c); }
+ var defs = el("defs");
+ [["tmah", HONCHO], ["tmas", HIND]].forEach(function (p) { var m = el("marker", { id: p[0], markerWidth: 7, markerHeight: 7, refX: 6, refY: 3, orient: "auto" }); m.appendChild(el("path", { d: "M0,0 L6,3 L0,6 Z", fill: p[1] })); defs.appendChild(m); });
+ svg.appendChild(defs);
+ svg.appendChild(el("line", { x1: W / 2, y1: 14, x2: W / 2, y2: H - 14, stroke: "#2a323f", "stroke-dasharray": "3 5" }));
+ lbl(midX / 2, 30, "HONCHO · Dialectic", HONCHO, 12, "middle", 700);
+ box(midX / 2 - 70, 52, 140, 34, HONCHO, "query");
+ box(midX / 2 - 70, 118, 140, 34, HONCHO, "LLM agent");
+ box(midX / 2 - 70, 184, 140, 34, HONCHO, "tools: search · recent");
+ svg.appendChild(el("path", { d: "M " + (midX / 2) + " 86 L " + (midX / 2) + " 118", stroke: HONCHO, "stroke-width": 1.3, "marker-end": "url(#tmah)" }));
+ svg.appendChild(el("path", { d: "M " + (midX / 2) + " 152 L " + (midX / 2) + " 184", stroke: HONCHO, "stroke-width": 1.3, "marker-end": "url(#tmah)" }));
+ svg.appendChild(el("path", { d: "M " + (midX / 2 - 70) + " 201 C " + (midX / 2 - 130) + " 201, " + (midX / 2 - 130) + " 135, " + (midX / 2 - 70) + " 135", stroke: HONCHO, "stroke-width": 1.3, fill: "none", "marker-end": "url(#tmah)", "stroke-dasharray": "4 4" }));
+ lbl(midX / 2 - 120, 170, "loop", HONCHO, 10);
+ box(midX / 2 - 70, 250, 140, 30, HONCHO, "answer");
+ svg.appendChild(el("path", { d: "M " + (midX / 2) + " 218 L " + (midX / 2) + " 250", stroke: HONCHO, "stroke-width": 1.3, "marker-end": "url(#tmah)" }));
+ var sx = W / 2 + (W / 2) / 2;
+ lbl(sx, 30, "HINDSIGHT · TEMPR", HIND, 12, "middle", 700);
+ ["semantic", "bm25", "graph", "temporal"].forEach(function (a, i) {
+ var seg = (W / 2 - 36) / 4, x = W / 2 + 18 + i * seg;
+ box(x, 52, seg - 6, 30, HIND, a);
+ svg.appendChild(el("path", { d: "M " + (x + (seg - 6) / 2) + " 82 L " + sx + " 118", stroke: HIND, "stroke-width": 1, opacity: .7 }));
+ });
+ box(sx - 70, 118, 140, 30, HIND, "RRF k=60");
+ svg.appendChild(el("path", { d: "M " + sx + " 148 L " + sx + " 178", stroke: HIND, "stroke-width": 1.3, "marker-end": "url(#tmas)" }));
+ box(sx - 70, 178, 140, 30, HIND, "cross-encoder");
+ svg.appendChild(el("path", { d: "M " + sx + " 208 L " + sx + " 238", stroke: HIND, "stroke-width": 1.3, "marker-end": "url(#tmas)" }));
+ box(sx - 70, 238, 140, 30, HIND, "token budget → facts");
+ host.appendChild(svg);
+ })();
+
+ /* ---------- surprisal ---------- */
+ (function surp() {
+ var host = byId("tm-surprisal"); if (!host) return;
+ var W = Math.min(host.offsetWidth || 640, 760), H = 320;
+ var svg = el("svg", { viewBox: "0 0 " + W + " " + H, width: "100%" }); host.appendChild(svg);
+ var clusters = [[W * .32, H * .42], [W * .6, H * .62], [W * .5, H * .32]];
+ var pts = [];
+ function gauss() { var u = 0, v = 0; while (!u) u = Math.random(); while (!v) v = Math.random(); return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); }
+ function nearest(p) { var m = 1e9; for (var i = 0; i < pts.length; i++) { var q = pts[i]; if (q === p) continue; var d = (q.x - p.x) * (q.x - p.x) + (q.y - p.y) * (q.y - p.y); if (d < m) m = d; } return Math.sqrt(m); }
+ function render(flash) {
+ while (svg.firstChild) svg.removeChild(svg.firstChild);
+ var g = el("text", { x: 12, y: H - 12, fill: "#6B7384", "font-size": 11 }); g.textContent = "embedding space (2-D projection)"; svg.appendChild(g);
+ pts.forEach(function (p) {
+ var s = nearest(p), high = s > 55;
+ var col = high ? HONCHO : (p.base ? "#3a4250" : "#6b7384");
+ var r = p.base ? 3.4 : 5;
+ svg.appendChild(el("circle", { cx: p.x, cy: p.y, r: r, fill: high ? col : "none", stroke: col, "stroke-width": 1.6, opacity: high ? 1 : .65 }));
+ if (p === flash) {
+ var ring = el("circle", { cx: p.x, cy: p.y, r: 6, fill: "none", stroke: high ? HONCHO : "#6b7384", "stroke-width": 1.5 });
+ svg.appendChild(ring);
+ if (!reduce) {
+ ring.appendChild(el("animate", { attributeName: "r", from: 6, to: 34, dur: "1s", fill: "freeze" }));
+ ring.appendChild(el("animate", { attributeName: "opacity", from: .9, to: 0, dur: "1s", fill: "freeze" }));
+ }
+ var t = el("text", { x: p.x + 12, y: p.y + 4, fill: high ? HONCHO : "#6b7384", "font-size": 11, "font-weight": 600 });
+ t.textContent = high ? "S=" + s.toFixed(0) + " · novel → reason" : "S=" + s.toFixed(0) + " · redundant → skip";
+ svg.appendChild(t);
+ }
+ });
+ }
+ function seed() { pts = []; clusters.forEach(function (c) { for (var i = 0; i < 14; i++) pts.push({ x: c[0] + gauss() * 28, y: c[1] + gauss() * 24, base: true }); }); render(); }
+ var add = byId("tm-surp-add"), rst = byId("tm-surp-reset");
+ if (add) add.onclick = function () {
+ var p;
+ if (Math.random() < .5) { var c = clusters[Math.floor(Math.random() * 3)]; p = { x: c[0] + gauss() * 26, y: c[1] + gauss() * 22, base: false }; }
+ else { p = { x: 60 + Math.random() * (W - 120), y: 40 + Math.random() * (H - 100), base: false }; }
+ pts.push(p); render(p);
+ };
+ if (rst) rst.onclick = seed;
+ seed();
+ })();
+
+ /* ---------- recency lab ---------- */
+ (function recency() {
+ var host = byId("tm-recency-svg"); if (!host) return;
+ var W = Math.min(host.offsetWidth || 400, 420), H = 230;
+ var svg = el("svg", { viewBox: "0 0 " + W + " " + H, width: "100%" }); host.appendChild(svg);
+ var aEl = byId("tm-alpha"), aV = byId("tm-alpha-val"), rEl = byId("tm-rel"), rV = byId("tm-rel-val"), verdict = byId("tm-recency-verdict");
+ function draw() {
+ while (svg.firstChild) svg.removeChild(svg.firstChild);
+ var alpha = parseFloat(aEl.value), rel = parseFloat(rEl.value);
+ aV.textContent = alpha.toFixed(2); rV.textContent = (rel >= 0 ? "+" : "") + rel.toFixed(2);
+ var baseCE = 0.6, janCE = baseCE + rel, junCE = baseCE, janR = 0.0, junR = 1.0;
+ var jan = janCE * (1 + alpha * (janR - 0.5)), jun = junCE * (1 + alpha * (junR - 0.5));
+ var top = 24, bot = H - 40, mid = (top + bot) / 2;
+ var bandTop = mid - (alpha / 0.6) * 70, bandBot = mid + (alpha / 0.6) * 70;
+ svg.appendChild(el("rect", { x: 40, y: bandTop, width: W - 80, height: bandBot - bandTop, fill: HIND, opacity: .07, rx: 4 }));
+ svg.appendChild(el("line", { x1: 40, y1: mid, x2: W - 40, y2: mid, stroke: "#2a323f", "stroke-dasharray": "3 5" }));
+ var t1 = el("text", { x: 44, y: bandTop - 6, fill: "#6B7384", "font-size": 10 }); t1.textContent = "±recency envelope"; svg.appendChild(t1);
+ var maxv = 0.95, minv = 0.35, scale = (bot - top) / (maxv - minv);
+ function bar(x, v, col, label, sub) {
+ var hgt = (v - minv) * scale, y = bot - hgt;
+ svg.appendChild(el("rect", { x: x - 34, y: y, width: 68, height: hgt, rx: 6, fill: col, opacity: .85 }));
+ var tv = el("text", { x: x, y: y - 8, fill: col, "font-size": 12, "font-weight": 700, "text-anchor": "middle" }); tv.textContent = v.toFixed(3); svg.appendChild(tv);
+ var tl = el("text", { x: x, y: bot + 16, fill: "#cfcabb", "font-size": 11, "text-anchor": "middle" }); tl.textContent = label; svg.appendChild(tl);
+ var ts = el("text", { x: x, y: bot + 30, fill: "#6B7384", "font-size": 9.5, "text-anchor": "middle" }); ts.textContent = sub; svg.appendChild(ts);
+ }
+ bar(W * 0.36, jan, HONCHO_DIM, "Jan · Python", "older fact");
+ bar(W * 0.64, jun, HIND, "Jun · Rust", "newer fact");
+ var winner = jun > jan ? "Rust (June)" : "Python (January)";
+ var margin = Math.abs(jun - jan) / Math.max(jun, jan) * 100;
+ verdict.innerHTML = 'recall returns: ' + winner + ' ' +
+ 'Jan score = ' + jan.toFixed(3) + ' Jun score = ' + jun.toFixed(3) + ' ' +
+ 'margin ' + margin.toFixed(1) + '% · ' +
+ (rel > 0.0001 ? ('relevance edge to January ' + (rel >= alpha ? 'overrides recency' : 'still losing to recency')) : 'relevance tied → recency decides');
+ }
+ aEl.addEventListener("input", draw); rEl.addEventListener("input", draw); draw();
+ })();
+
+ /* ---------- trace stepper ---------- */
+ (function trace() {
+ var H = [
+ ["Jan message stored", 'Deriver emits an explicit observation: "user loves Python" (+ deductive: "is a programmer"). No conflict yet.'],
+ ["Jun message stored", 'Deriver emits "user now prefers Rust." Two observations coexist; nothing reconciled.'],
+ ["Dream cycle fires", "Surprisal builds a tree over the peer's observation cloud. The Rust point sits in a sparse region → high surprisal → selected."],
+ ["Deduction specialist", "Discovery tools surface both facts. Recognizes a knowledge update: preference changed over time."],
+ ["Reconcile + prune", 'Writes a dated update with source_ids + premises; soft-deletes the stale "loves Python" observation.'],
+ ["Query: prefers?", 'Dialectic tool-loops, finds the current observation → answers "Rust, as of June", chain traceable via premises.']
+ ];
+ var S = [
+ ["Jan retained", "TEMPR extracts narrative fact f1 (τ≈Jan, entities {User,Python}); embedded, entity-linked."],
+ ["Jun retained", "f2 (τ≈Jun, entities {User,Rust,Python}). Shared Python entity → hard w=1.0 edge f1↔f2."],
+ ["Consolidation", "New evidence classified contradict vs the Python opinion → c′ = max(c−2α, 0); confidence collapses with repetition."],
+ ["Supersede", 'Old observation marked superseded → moved to invalidated_memory_units; new "prefers Rust" written with history.'],
+ ["Recall · 4 arms", "Semantic finds f1,f2; temporal + recency push f2 above f1; RRF fuses, cross-encoder reranks."],
+ ["Reflect → answer", 'CARA answers "Rust, as of June". f1 stays valid history — "used to prefer Python" still queryable.']
+ ];
+ var hHost = byId("tm-trace-h"), sHost = byId("tm-trace-s"); if (!hHost || !sHost) return;
+ function build(host, arr) { arr.forEach(function (s, i) { var d = document.createElement("div"); d.className = "tm-step"; d.dataset.i = i; d.innerHTML = '' + String(i + 1).padStart(2, "0") + '' + s[0] + ' ' + s[1] + ''; host.appendChild(d); }); }
+ build(hHost, H); build(sHost, S);
+ var total = H.length, cur = 0, timer = null;
+ var stepn = byId("tm-trace-stepn"), playBtn = byId("tm-trace-play");
+ function paint() { var n = document.querySelectorAll("#tm-trace-h .tm-step, #tm-trace-s .tm-step"); n.forEach(function (x) { x.classList.toggle("on", +x.dataset.i < cur); }); stepn.textContent = "step " + cur + " / " + total; }
+ function step() { if (cur < total) { cur++; paint(); } if (cur >= total) stop(); }
+ function play() { if (timer) { stop(); return; } playBtn.textContent = "❚❚ Pause"; if (cur >= total) cur = 0; timer = setInterval(step, 1100); }
+ function stop() { clearInterval(timer); timer = null; playBtn.textContent = "▶ Play"; }
+ playBtn.onclick = play;
+ byId("tm-trace-step").onclick = function () { stop(); step(); };
+ byId("tm-trace-reset").onclick = function () { stop(); cur = 0; paint(); };
+ paint();
+ })();
+})();
diff --git a/templates/base.html b/templates/base.html
index 6b7ee5f..ae5c811 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -18,8 +18,8 @@
-
-
+
+