← the Atlas

Awareness — the producer observes, kolu remembers

budding·implemented·

The converged awareness architecture (PR #1621), simplified across a long design review. One complect underlies every hard thing in R9 — the sensor engine DERIVES awareness by mutating a host record it also reads back as memory. The fix splits observing from remembering. A host PRODUCER emits one TerminalSnapshot = {cwd, git, pr, agent, foreground} — exactly what it can re-observe, and nothing it cannot. kolu stores TerminalState = that last-seen TerminalSnapshot plus TWO facts a host can never observe and only kolu may write — lastActivityAt (recency, on kolu's clock) and lastAgentCommand (the line to replay). The "fold" is last-write-wins for the five snapshot fields plus those two rules. Restore resumes an agent only if it was still live at sleep/restart (quit-to-shell wakes to a bare shell), which collapses agentSession into the frozen agent. Lie-when-dead is handled by the active/sleeping discriminant, not by dropping fields. This note pins the types, the API, and the flows.

The de-entanglement that makes R9 mostly evaporate. Every hard thing in the finale — the two assemblers, the createPulam boundary, the fold-clobber — traces to one complect: the sensor engine derives awareness by mutating a host record it also reads back as memory. Split observing from remembering and it dissolves. This note pins the types, the API, and the flows so the boundary can be evaluated before any code; every claim cites the code it restructures.

Landed (S1 + S2 → R9.0): #1626, then hardened by a 4-reviewer architecture-first-principles + perfection-review gauntlet. As shipped, the restore target is a fold-produced discriminated value on the authored record — RestoreTarget = none | { exact, command, agent } | { legacyMostRecent, command } (restoreTargetOf, consumed by resumeFormFor) — rather than a bare optional identity, so an absent field can never be misread as “resume most-recent”. Quit-to-shell → nonebare shell by construction for new records (the strict #1492 behavior); migrated pre-1.29 records keep their old most-recent resume as a named legacyMostRecent. Deferred (tracked, not shipped): the active drain-at-sleepbeginSleep freezes the last fold-written target and does not drain a final settle, so a launch-then-sleep inside the agent settle window freezes a stale none and wakes to a false bare shell (narrow, self-corrects on re-launch). The recency frame phase is now value-based (compare the re-resolved identity to the saved target), not the original wall-clock window. Follow-up fix (#1726): making recency identity-only froze it for a stable session — a terminal running one week-old session forever never changed identity, so its “last activity” stopped moving even while it produced output. Recency now bumps on a same-identity output tick too, throttled to RECENCY_THROTTLE_MS (60s) so the ~1s agent-detail firehose can’t recreate the per-tick write noise #1626 removed; the identity bump stays and the two compose. The recencyBaseline (seeded to the survivor’s identity) is kept — it’s what keeps the adopt re-observation from counting as new activity, and, being identity- not count-based, it holds across the survivor’s multi-emit settle burst and never swallows a later genuinely-new agent launch. The throttle clock is floored at the sensor run’s start (FoldCtx.runStartedAt), so that settle burst — clustered at run start — coalesces against it instead of false-bumping the (possibly week-old) saved recency.

The knot today — observing is fused with remembering

The sensor engine does not return awareness. It takes a host AwarenessSink + a per-terminal AwarenessRecord, mutates record.meta through the sink, and reads record.meta back as its own prior state. sensors.ts:139-162 calls this the load-bearing apply-and-publish contract — a sink that publishes without mutating the record “would silently defeat every dedup/transition gate.”

So record.meta plays two roles at once:

Today — deriving is fused with storing record.meta is the engine's prior-state AND the host's store — one object, two roles sensor set (the engine) git→pr · agent×3 · foreground record.meta : AwarenessValue ▲ read back — dedup · recency · session gates ▼ mutated in place = the store mutate via sink ▼ ▲ read prior-state kolu host — its own sink entry.awareness (registry) · fold · persist + trackRecent pulam host — its own sink its cache · publish (ephemeral) record IS this record IS this Each home wires its OWN sink + record ⟹ two assemblers; and because deriving writes the store, a fresh derivation can clobber remembered state.
Today: the sensor set and the host's storage are the SAME object. record.meta is both the engine's prior-state (read back by the dedup/recency/session gates) and the host's stored value (mutated in place via the sink). Because deriving writes the store, a fresh derivation can clobber remembered state, two sinks must each wire the assembly, and 'share the assembly' (createPulam) is the only way two homes can reuse the sensing.

That fusion is the cause of everything downstream: two assemblers (local.ts:278 vs pulam/src/hooks.ts:39), the AwarenessSink foot-gun (mutate-before-publish or defeat the gates), and the fold-clobber class (a fresh/ephemeral derivation overwrites remembered history — the defect that killed the local-pulam-process #1614 and that the createPulam loop re-risked).

The cut: one question — can a host re-observe it?

There are not three categories. There is one question per field, and it sorts every field into exactly two places:

// What a host PRODUCER emits — exactly what it can RE-OBSERVE. Local or remote, the SAME type.
type TerminalSnapshot = {
  cwd: string;
  git: GitInfo | null;
  pr: PrResult;
  agent: AgentInfo | null;        // the LIVE agent right now, or null when the user is at the shell
  foreground: Foreground | null;  // the live foreground process (vim, …)
};

// The two facts a host CANNOT observe — recency is a clock reading; the launch line is what the
// user typed. Irrecoverable from a screen, so kolu remembers them; written by kolu's fold ALONE.
type AgentMemory = {
  lastActivityAt: number;         // recency — a CLOCK reading. You can't see "active 3h ago" on a screen.
  lastAgentCommand?: string;      // the exact launch line to replay ("claude --model sonnet"). Absent if no agent ran.
};

// kolu's stored value: the last-seen TerminalSnapshot + the two remembered facts. NESTED, not merged,
// so the half published to the snapshots collection is `current.snapshot` — structurally WITHOUT
// the two memory fields, not a runtime strip.
type TerminalState = { snapshot: TerminalSnapshot; memory: AgentMemory };

TerminalSnapshot is the keystone type: a producer — local or remote — cannot construct lastActivityAt or lastAgentCommand, so a producer’s stream (however buggy, restarted, or hostile) cannot overwrite kolu’s two remembered facts, by the compiler. The persisted/live partition (schema.ts:139-176) was too coarse; the simpler structure is observable vs not, and the only fence that must be a type is that one — the producer’s emit type is TerminalSnapshot, full stop.

S1 — the producer is memoryless

It owns transient derivation working state (the agent watcher, latestInfo, the last-emitted live mirror, currentAgent, the git→PR wire, the screen-scrape poll, the adapter registry — all re-seeded empty each start) and emits per-field observations. It takes no seed and touches no host store. The standing five build the TerminalSnapshot; commandRun is a discrete mark that feeds kolu’s lastAgentCommand memory and the recent-agent MRU.

// The agent is the one field that resolves ASYNCHRONOUSLY — the session file lands a beat after
// the mark, over a settle window (`COMMAND_RUN_RECONCILE_DELAYS_MS = [0,75,300,1000]`, `sensors.ts:486`;
// `SCREEN_SCRAPE_POLL_MS = 1000`, `:492`). So a bare `agent: null` is ambiguous — "no agent" or "not
// resolved yet?" Make the null LAWFUL (round-6 fix): a producer mid-resolution emits `unknown` (kolu
// KEEPS its last value); `{ value }` is authoritative (kolu APPLIES it, even when null). The authority
// for `{ value: null }` is shell-idle — the foreground IS the shell (`sensors.ts:432,448`), i.e. the
// session ended (`sensors.ts:640-644`); a value can't be both "agent live" and "shell idle."
type Known<T> = "unknown" | { value: T };

type TerminalEvent =
  | { kind: "cwd";        cwd: string }
  | { kind: "git";        git: GitInfo | null }                  // also drives the recentRepo MRU (kolu side)
  | { kind: "pr";         pr: PrResult }
  | { kind: "foreground"; foreground: Foreground | null }
  | { kind: "agent";      agent: Known<AgentInfo | null> }    // async-resolved → lawful unknown/snapshot, never a bare null
  | { kind: "commandRun"; command: string; replayed: boolean };  // feeds lastAgentCommand + recentAgent MRU

function startSensors(
  id: TerminalId,
  inputs: {
    pid: number;
    cwd: string;                                          // spawn-time cwd; later cwd flows via signals
    signals: SensorSignals;                            // the four taps — UNCHANGED
    readScreenText?: (tailLines: number) => Promise<string>;  // pure input for screen-scrape (#905)
    log: Logger;
  },
  emit: (o: TerminalEvent) => void,
): () => void;

replayed is the one provenance flag on an observation, and it must be a required field (a flag-less event is a compile error). It is genuine content — kaval’s tap (sensors.ts:103-106) reports whether a command mark is a replay (scrollback re-emit on restart) or a live run; the producer does not guess it. There is no fromSnapshot: “is this a re-observation or a live change” is not a property of the event — it’s kolu’s subscription-phase fact, carried on the frame (the wire), never a per-event flag a producer asserts and kolu trusts.

Deleted: the AwarenessSink interface, both makeAwarenessSink impls, the AwarenessRecord type, the apply-and-publish read-back contract — and any seed. The git→PR channel and the adapter registry were always engine-internal and stay.

S2 — kolu remembers; the fold owns the two facts and the clock

kolu folds each observation into a new TerminalState. The five snapshot fields are last-write-wins; the only decisions are the two remembered fields — stamp lastActivityAt on a live agent-identity change (kolu’s clock), and keep lastAgentCommand from the latest commandRun. A producer can write none of that. (The identity-only recency shown below froze a stable session’s recency; it now bumps on a throttled same-identity output tick too — see the follow-up fix in the status note above.)

// Liveness + clock are kolu's, passed as VALUES (not a thunk the reducer may fire):
//   live — true iff this came in a DELTA frame (a snapshot re-observation is not "new activity")
//   at   — kolu samples its own clock ONCE at intake, before folding
type FoldCtx = { live: boolean; at: number };

function fold(cur: TerminalState, o: TerminalEvent, ctx: FoldCtx): TerminalState {
  const obs = (p: Partial<TerminalSnapshot>) => ({ ...cur, snapshot: { ...cur.snapshot, ...p } });
  switch (o.kind) {
    case "cwd":        return obs({ cwd: o.cwd });
    case "git":        return obs({ git: o.git });
    case "pr":         return obs({ pr: o.pr });           // pr is true-when-dead — persisted like git
    case "foreground": return obs({ foreground: o.foreground });

    case "agent": {
      if (o.agent === "unknown") return cur;               // not re-resolved yet → KEEP kolu's last value (no clobber)
      const agent = o.agent.value;                          // authoritative (incl. a shell-idle null = session ended)
      const next = obs({ agent });                          // last-write-wins
      // RECENCY (decided: identity-only): bump iff a LIVE agent-IDENTITY change — start / finish /
      // new session. ctx.live (the frame phase), not a producer flag, says "live"; kolu's clock stamps it.
      const idChanged = cur.snapshot.agent?.kind      !== agent?.kind
                     || cur.snapshot.agent?.sessionId !== agent?.sessionId;
      return ctx.live && idChanged
        ? { ...next, memory: { ...next.memory, lastActivityAt: ctx.at } }
        : next;
    }

    case "commandRun":                                     // the producer emits this ONLY for a recognized agent
      return cur.memory.lastAgentCommand === o.command     //   command, already NORMALIZED (`sensors.ts:404-409`) — a
        ? cur                                              //   non-agent `ls` yields no event, so it can't clobber.
        : { ...cur, memory: { ...cur.memory, lastAgentCommand: o.command } };  // dedup (`sensors.ts:410`): a replay is a no-op
  }
}
// kolu's host recipe. Autosave (disk) arms ONLY when a RESTORE-RELEVANT value changes — the
// five snapshot fields minus the churny ones, plus the two memory fields. Agent DETAIL (state,
// tokens, summary) and `foreground` change every ~150 ms and are re-derived on (re)spawn, so they
// never touch disk: the firehose can't reach it, because what arms is a VALUE CHANGE, not a tick.
const restoreRelevant = (a: TerminalState) => ({
  cwd: a.snapshot.cwd, git: a.snapshot.git, pr: a.snapshot.pr,
  agentId: a.snapshot.agent && { kind: a.snapshot.agent.kind, sessionId: a.snapshot.agent.sessionId }, // IDENTITY only
  ...a.memory,
});

function watch(key: HostTerminalKey, terminal): () => void {
  let current = seed(terminal);                            // kolu seeds from its durable record
  const stop = subscribeSnapshots(key, terminal, (frame) => {          // frame: snapshot | delta | gap (the wire)
    if (frame.phase === "gap") { current = reseed(key); return; }      // a detected gap re-snapshots — never a silent diverge
    const ctx: FoldCtx = { live: frame.phase === "delta", at: Date.now() };  // kolu's clock, sampled once
    for (const e of frame.events) {
      const before = current;
      current = fold(current, e, ctx);                     // a NEW value; nothing mutated
      if (!shallowEqual(restoreRelevant(before), restoreRelevant(current))) armAutosave();  // value change, not tick
      if (e.kind === "git" && e.git)            trackRecentRepo(e.git.mainRepoRoot, e.git.repoName);  // MRU, kolu side
      if (e.kind === "commandRun" && !e.replayed) trackRecentAgent(e.command);                        // replay never MRUs
    }
    snapshots.upsert(key, current.snapshot);               // the snapshots collection = TerminalSnapshot (memory absent by type)
    authored.updateMemory(key, current.memory);            // the ONE memory writer (a narrowed mutator)
  });
  return () => { stop(); /* late events after stop are ignored — the post-teardown no-op guard */ };
}
S2 (R9.0) — the producer observes, kolu remembers kaval taps cwd · title · commandRun · foreground memoryless producer emits events · no seed TerminalSnapshot only cannot spell the 2 memory fields TerminalEvent cwd · git · pr · foreground agent (snapshot|delta from frame) commandRun { replayed } — no memory fields — recentRepo/Agent: fold-derived kolu's fold last-write-wins + 2 kolu-only fields kolu's clock · identity-only recency arm autosave = restore-relevant value changed events snapshots collection TerminalSnapshot {cwd·git·pr·agent·fg} — no memory kolu.authored memory: lastActivityAt · lastAgentCommand snapshot memory browser — authored ⋈ snapshots (unchanged) no record handed back · no sink · no seed · no clobber — and a producer cannot spell memory, by type
S2 (R9.0), local. The memoryless producer emits per-field observations (no seed, no memory). kolu's fold seeds current from its durable record and folds each framed observation: the five snapshot fields are last-write-wins; lastActivityAt is stamped on a LIVE (delta-frame) agent-identity change with kolu's clock; lastAgentCommand is remembered from the commandRun mark. It publishes the snapshot half to the snapshots collection (TerminalSnapshot — the two memory fields are absent by type) and the memory half to kolu.authored. Autosave arms only when the RESTORE-RELEVANT projection (cwd, git, pr, agent identity, the two memory fields) changes — agent detail and foreground churn never reach disk, so the firehose can't. One way: observe → fold → store. No record handed back, no sink, no seed, no clobber.

Persistence tiers — what lives where

The types say who may write a field; they don’t yet say where it survives. Three tiers, and the disk/RAM line is — honestly — the old cache/live split wearing a new coat (round-6 confirmed it doesn’t vanish; it moves from a field-typed split to the restoreRelevant projection):

tier what why
server disk (survives kolu restart) the restoreRelevant projection: cwd · git · pr · agent identity { kind, sessionId } · lastActivityAt · lastAgentCommand the minimum to re-seed and to honor model-B restore. Agent identity only — not the full AgentInfo (no lie-when-dead state/tokens on disk; the round-5 invariant survives the merge). Autosave arms on a change to this projection, so the ~150 ms firehose never reaches disk.
server RAM (re-derived on (re)spawn) the rest of snapshot: full AgentInfo detail (state, summary, tokens, taskProgress) · foreground lie-when-dead and high-churn. A respawned PTY re-derives it in ~1 s; losing it on crash costs nothing, so it never earns a disk write.
browser localStorage (per-device, never server) client view-state — attention/unread (useViewState.ts), zoom/layout, per-device prefs (persistedPref.ts) a viewing posture, not a fact about the terminal. It must not round-trip through the server (it’s device-local), and the server must never treat it as awareness.

So the simplification deletes the names cache/live but not the underlying categories — it relocates them: cache→restoreRelevant, live→RAM-only. The honest claim is “persistence is the restoreRelevant projection,” not “everything in TerminalSnapshot persists.”

The wire — observations, not whole values

The collection serving a whole AwarenessValue (surface.ts:119-124) is retired as the awareness primitive. Producers serve an ordered, framed terminalEvents stream (host-scoped, snapshot-then-deltas, subscribe-before-serialize — the attach contract, router.ts:162-169, and the streaming.md rule) of TerminalSnapshot events. The wire carries the fold’s input, not its lossy output. There is no observedAt on the wire (a clock-skew temptation) — kolu samples its own clock at intake.

The stream is framed — the frame carries provenance and order so the observation vocabulary can stay field-level (the streaming.md-prescribed shape, and the round-5 hardening fix):

type TerminalFrame =
  | { phase: "snapshot"; events: TerminalEvent[] }            // ctx.live = false — a re-observation, never bumps recency
  | { phase: "delta";    seq: number; events: TerminalEvent[] }  // ctx.live = true; seq is monotonic per subscription
  | { phase: "gap";      afterSeq: number };                               // a detected coalescing/overflow gap → kolu re-snapshots

Two facts the frame owns that a per-event flag must not:

kolu consumes the framed stream through one subscribeSnapshots(key, …) seam regardless of transport: locally it wraps the in-process startSensors (the initial emit burst is the snapshot frame, later emits are deltas); remotely it is the ssh stream verbatim. So the framing — not the engine — carries phase/seq, and the engine stays a pure memoryless emitter. But ctx.live is not the frame phase alone: it is (phase === "delta") AND kolu’s durable agent-identity baseline check, derived in kolu’s consumer arm. A snapshot re-observation is never live; and a delta that merely re-resolves the adopted agent — which seeds null, the resume identity riding the authored restoreTarget, not the snapshot — must not bump recency either. The framer alone can’t decide liveness; only kolu, holding the durable record, can. (Settled by the R9.2/R9.3 debate, debates/r92-r93/.)

The payoff: kolu’s fold is one function for local and remote, and reconcileRemoteSnapshot disappears. The snapshot frame meets a pre-seeded current and folds with ctx.live = false (no spurious recency bump). adoptedAwareness (local.ts:340), wake, remote-connect, and remote-reconnect are the same “seed from memory, then fold” — orphanAwareness (local.ts:368) is the degenerate zero-memory seed.

How R9 collapses

A “host” is now dial a kaval → per terminal: run the memoryless producer → serve its observation stream. kolu is the one consumer that remembers: it folds every host’s stream (localhost in-process, remotes over ssh) into a host-keyed aggregate. “Two homes” was an artifact — there is one producer per host; localhost is the degenerate (in-process) host.

S1 · S2 → R9 collapses✓ shipped · ▶ do next · ◐ build-clean · ○ todo · → drill in
S1 · memoryless producer — observations, no seed, TerminalSnapshot onlyshipped #1626
S2 · kolu's fold — last-write-wins + 2 kolu-only fields · kolu's clock · memory→authoredshipped #1626
R9·lib · DISSOLVES — nothing to assemble; producer + fold are pure leavessubsumed by S1+S2
R9.0 · kolu runs the producer in-process, folds onto authored+snapshotsshipped #1626 (foundation assessed transport-agnostic)
PR-3 · awareness host-ready — framer + terminalEvents wire (prep · local no-op)needs S2 · ∥ PR-1 PR-2
F-REMOTE · the complete remote tile — folds the remote terminalEvents through the SAME fold (+ PTY · fs/git · restore · …)needs PR-1 PR-2 PR-3 · R7
PR-1 · PR-2 · R10 (lifecycle · fs/git · host-picker UX)→ finale
phase was now
R9·lib share a daemon-shaped assembly (createPulam) dissolved — producer + fold are pure leaves; each host’s loop is ~8 lines
R9.0 kolu cuts over its sink/loop; clobber risk ✓ shipped (#1626) — producer in-process, folds onto authored+snapshots; clobber-free by type
remote awareness unbuilt; “the remote-only fold” re-sequenced (debates/r92-r93/): the producer-side prep is PR-3 (framer + the terminalEvents wire, a local no-op), and F-REMOTE subscribes the remote stream → the same fold. No reconcileRemoteSnapshot. Work-list below.
the rest PR-1 (lifecycle) · PR-2 (fs/git) prep in parallel; F-REMOTE lights the complete tile; R10 the host-picker UX — see the finale
Same producer, same fold — two transports (local R9.0 · remote R9.3) LOCAL (R9.0) — kolu-server memoryless producer — in-process ← kaval taps (local socket) kolu's fold kolu's clock · seeds from its record snapshots = TerminalSnapshot + memory → kolu.authored browser authored ⋈ snapshots REMOTE (R9.3) pulam daemon (degenerate host — no memory) memoryless producer — SAME ← remote kaval taps serve: terminalEvents stream TerminalEvent · snapshot-then-deltas kolu: subscribe stream → the SAME fold kolu's clock · seeds from its record snapshot frame IS the reconcile — no reconcileRemoteSnapshot ssh · events (lossless) identical producer + fold kolu seeds current from its durable record either way, so the snapshot frame is the reconcile — one fold, two transports. Memory → kolu.authored; the snapshots collection is TerminalSnapshot everywhere.
Local (R9.0, in-process) vs remote (F-REMOTE) — the SAME memoryless producer and the SAME fold, two transports. LOCAL: kolu runs the producer in-process; its observations feed kolu's fold directly. REMOTE: the producer runs in the pulam daemon and serves an ordered TerminalSnapshot event stream over ssh; kolu subscribes and folds with ITS clock. Either way kolu seeds current from its durable record, so the snapshot frame is the reconcile — there is no separate reconcileRemoteSnapshot. The two memory fields land on kolu.authored; the snapshots collection is TerminalSnapshot everywhere. Reader-join (authored ⋈ snapshots) unchanged.

Remote awareness — the build (PR-3 prep + F-REMOTE consume)

R9.0 shipped (#1626). Before building remote awareness it was assessed by a 4-reviewer foundation debate (debates/r93-foundation/): the verdict is clean / transport-agnostic — nothing in R9.0 must be undone. Verified against the merged diff:

So remote awareness is an additive transport arm, not a refactor — and the re-sequence (debates/r92-r93/) places the producer-side prep in PR-3 (a pure local no-op) and the consume in F-REMOTE (sharing the pulam dial F-REMOTE already holds for fs/git). Mapping the work-list below: items 1–3 are PR-3 (the framer, the wire, pulam serving events); items 4–5 are F-REMOTE (kolu subscribes + folds, HostTerminalKey). The work-list, given R9.0’s actual structure:

  1. Build the TerminalFrame type (designed above — snapshot | delta(seq) | gap). R9.0 has only TerminalEvent; the framed stream was deferred here, by design. (PR-3)
  2. A framer that turns the producer’s emit sequence into frames — wraps the producer (which stays a pure emitter) and assigns phase/seq only. It is not where ctx.live is decided: liveness is (phase === "delta") AND kolu’s durable agent-identity baseline check, in kolu’s consumer arm (the framer can’t see kolu’s restoreTarget; the remote framer runs in pulam, which has no kolu memory). Deleting the baseline would spuriously bump recency on every restart — adopt re-seeds agent: null and the agent re-resolves asynchronously as a delta. (PR-3 — pin the adopt case with a differential test.)
  3. Pulam serves the framed event stream — not just its snapshot cache. ⚠️ The sharp, non-obvious requirement (the debate’s headline): pulam today folds each TerminalEvent into a TerminalSnapshot cache and serves that (perfect for pulam-tui/pulam-web, which need no recency). But that cache is insufficient for kolu-as-remote-consumer, because commandRun is a memory mark that foldSnapshot drops — so the remote daemon must serve the raw event stream (a new terminalEvents stream beside the snapshots collection, contract bump 3.0 → 3.1, additive), and kolu must not rebuild memory from the served snapshots. (PR-3)
  4. kolu’s remote subscribe-and-fold arm — dial the remote pulam (it serves the terminal-workspace surface — kaval serves only PTY; getHostSession({binary:"pulam"}), the pulam-tui precedent, bake PULAM_AGENT_DRVS_JSON), mirror the terminalEvents over R7’s total dual, seed current from kolu’s durable record, fold exactly as local; on a missing seq, force a gap → re-snapshot. (F-REMOTE)
  5. Host-scoped keys — the branded HostTerminalKey (designed in “Decided”), kolu-intake-only (the wire stays bare TerminalId), lands here, where two hosts’ opaque PTY ids can actually collide. (F-REMOTE)

Order: PR-3 (this awareness producer-prep) lands in parallel with PR-1 (lifecycle) and PR-2 (fs/git) — all three pure local no-ops — then F-REMOTE subscribes the remote terminalEvents on the pulam mirror it already holds for fs/git and folds it through the same fold, lighting remote awareness as part of the one complete-everything tile. See the finale.

Decided — the forks closed

Phasing & migration

The one thing no type settles — and its containment

Exactly one judgment is a product choice, not a type: which agent transition counts as “activity” (two composing arms since #1726: an identity change always, plus a throttled same-identity output tick — see the follow-up fix at the top). It lives in the fold’s tested branches with no producer bypass. Almost everything else is unspellable by type; the single honest exception is the autosave fence, which rides a restore-relevant value change (a Partial<T> admits {}) — so it gets a test, not a type. Tests that make that real: