diff --git a/BUILT.md b/BUILT.md
index 421fd9d..aae38fb 100644
--- a/BUILT.md
+++ b/BUILT.md
@@ -4872,6 +4872,19 @@ invariant the whole of `flan_dev.c`'s watch section rests on. The cost is that a
next runs rather than at the instant of the reset — and for a frame loop that is the only moment it could sensibly
begin. The test waits for it rather than reading once, and says why.
+**The lazy clear stays lazy, and a stopped program is the case that decides it.** The obvious tidy-up is to make the
+reader epoch-aware — compare the slot's epoch with the global one under the seqlock and report an untouched slot as
+empty — which would make a reset visible in the very next tick instead of in the tick after the program's next
+sample. It is the wrong trade, because **a stopped program does not sample**. An epoch-aware reader would blank the
+watch for as long as the program sat in a break loop, and reading the numbers from the moment you stopped is the
+entire point of stopping. The lazy clear gives exactly the right answer there. What was actually wrong was narrower
+and lives in the editor: `flan-watch--tick` was sending `:reset t` five times a second at a program that could not
+answer it. So the reset is now guarded on `flan-dev--stopped` — the read still goes out every tick, only the reset
+field drops — and the runtime is untouched. That keeps the policy where the rest of this section already put it:
+"since you last looked" is the editor's idea, not the table's. `watch_render_num`'s unreachable `n=0` arm is deleted
+rather than commented, since the only way to reach it is the epoch check that was just rejected, and dead code is an
+invitation to add one.
+
**An `i64` accumulates as a double**, so a magnitude past 2^53 loses precision in the sum and in the ends of the
range. Recorded rather than designed around: a count, a coordinate and a tile index are what this is pointed at, and
a second integer accumulator for a case nobody has would be two code paths for one tool. `watch-num-i64` and
diff --git a/HANDOFF-f3.md b/HANDOFF-f3.md
deleted file mode 100644
index a1015ab..0000000
--- a/HANDOFF-f3.md
+++ /dev/null
@@ -1,252 +0,0 @@
-# Handoff — a watch for a running program (`PORTING.md` Tier 1, item 5)
-
-Written because the session was wound down for budget. **The work is finished and `dune test` is green**, run twice.
-This file exists so the reasoning is not re-derived, and so the two things I would have done next are named.
-
----
-
-## The finding that reframes the task
-
-**Most of item 5 was already built before this session.** The brief reads as though a watch had to be built from
-nothing; it did not. What existed at `344e571`:
-
-- `runtime/flan_dev.c` — the watch table: 64 slots, a name and rendered text per slot, a per-slot seqlock, an
- `on`/`off` flag so a watch call costs a load and a not-taken branch when nobody is looking, and four scalar entry
- points (`flan_dev_watch_i64`/`_u64`/`_f64`/`_str`).
-- `vendor/agent/flan_agent.c` — `watch`, `watch on`, `watch off` on the agent socket.
-- `lib/dev.ml` — `watch_read`, `watch_enable`, and the `:op "watch"` / `:op "watch-enable"` arms.
-- `emacs/flan-watch.el` — the watch buffer *and* the inline ghost text, both fed from one reply.
-- `test/test_dev.ml` — a block driving a real daemon and a real running program.
-
-That is `watch.clj`'s `spy`, end to end. **What was missing was `spy-num`** — the numeric accumulator for hot loops,
-which is precisely the part `PORTING.md` calls "least obvious and most valuable". That is what this session built.
-
----
-
-## The accumulator decision
-
-This is the expensive part to re-derive, so it is stated in full. The long-form version is in `BUILT.md`, "A hot loop
-keeps five numbers, and the window is the editor's"; `runtime/flan_dev.c` carries it at the code.
-
-### What a slot keeps
-
-**Count, min, max, last, mean.** Five numbers per label.
-
-The scalar watch keeps one value per name. Sampled from a hot inner loop that is nearly useless — you see whichever
-of the 91,200 cells happened to run last. `watch.clj`'s own docstring says so and is why `spy-num` exists there.
-
-Each of the five answers a question you can ask *without building a query*:
-
-- **`n`** — how many times the expression actually ran. This is the first thing that is wrong when a loop is wrong. A
- count that tracks the frame counter rather than the cell count is a loop that is not running.
-- **`min` / `max`** — the range. This is the thing a single sample can never show you, and it is what you are looking
- for when you suspect an index or a velocity is leaving the region it should stay in.
-- **`last`** — the one sample. Kept because it is what the scalar watch would have given you, and losing it on the
- way to something richer would be a regression.
-- **`mean`** — carried as a running `sum` and divided at read time. A mean accumulated as a mean drifts; a sum does
- not.
-
-### What it deliberately does not keep
-
-**A ring, or a history.** A small ring of the last N samples was the serious alternative. It loses on the only ground
-that matters here: N samples out of 91,200 is a sample of the *tail* of the loop, not of the loop. It answers "what
-did the last few cells do" when the question is "what did the cells do". Past five numbers, every richer answer is a
-UI for building a query — and a query builder is the one thing this whole design exists not to be. The ghost text
-section of `BUILT.md` already settled the same question the same way for the scalar case ("a watch inside a loop
-shows the last value written... every better answer is a UI for building a query. Settled, not open.").
-
-### The write path does no formatting
-
-This is the feature, not an optimisation. An `snprintf` per sample at thousands of samples a frame is a HUD that
-costs more than the game. A sample is: one relaxed load of the epoch, five compares and stores, and the slot's
-seqlock. The **reader** — the agent's listener thread, once per editor tick — turns the five numbers into text.
-`watch.clj` reaches the same place with a `double-array` per label and a `render` function Emacs calls.
-
-This is also why the slot grew a `num` flag rather than getting a second table: the read path already copies under a
-seqlock, so it copies five doubles instead of 192 bytes and renders them out of locals *after* the counter check.
-
-### The window is since the last reset — a deliberate divergence from `watch.clj`
-
-`watch.clj` is cumulative until `reset-spies!` is called by hand. **This is not**, and the disagreement is on purpose.
-
-Cumulative is the wrong default for a frame loop. A `min` and a `max` over a whole session reach the session's
-extremes within a few seconds of play and then never move again — so the two most useful of the five numbers go dead
-*exactly* when you start interacting with the thing you are debugging. This tool exists to show you a number while
-you drag the mouse. So `flan-watch--tick` sends `:reset t` beside its read, and what you see is "since you last
-looked": a fifth of a second, about a dozen frames, which keeps the range tracking the present.
-
-A caller who wants cumulative numbers gets them by not resetting. The setting lives in the editor, not the runtime.
-
-### Reset is its own message, never a side effect of reading
-
-A destructive read was the tempting shape and is wrong: it makes *looking* change what is there. Anything that polls
-— a test's `await`, a second editor, a person reading twice — would silently shorten the window and come back with a
-count that is noise. So `watch reset` is its own agent command, `:reset t` is a field on the read op, and `dev.ml`
-sends the reset *after* the read so a tick reports the window it just closed.
-
-This was settled before the test was written rather than after, which is the only reason the test can assert anything
-about `n` at all.
-
-### The reader never writes the table
-
-Reset bumps one global epoch counter and touches no slot. A slot clears itself on its **next sample**, when it
-notices the epoch moved, and does so *inside* its own odd-generation window so a reader can never catch a
-half-cleared slot. The game thread stays the only writer of the table — the invariant the whole watch section of
-`flan_dev.c` rests on.
-
-The cost: a new window begins when the program next runs, not at the instant of the reset. For a frame loop that is
-the only moment it could sensibly begin. **This bit me in the test** (see "What did not work" below) and the test now
-waits for it and says why.
-
-### `i64` accumulates as a double
-
-A magnitude past 2^53 loses precision in the sum and in the ends of the range. Recorded rather than designed around: a
-count, a coordinate and a tile index are what this is pointed at, and a second integer accumulator for a case nobody
-has would be two code paths for one tool. `watch-num-i64` and `watch-num-f64` are two entry points only so a program
-need not cast at the call site.
-
-### A whole number prints as one
-
-`watch.clj`'s `fmt-num` does this and the reason survives the port: a watch on an array index that reads `66.0000`
-sends you looking for a rounding bug that is not there.
-
----
-
-## What was built, file by file
-
-All of it is **working** — built, run against a real daemon and a real running program, and covered by the test.
-Nothing is stubbed and nothing is half-written.
-
-| File | Change | State |
-|---|---|---|
-| `runtime/flan_dev.c` | `watch_slot` grew `num`, `epoch` and five doubles; `watch_begin` clears `num`; new section "A number sampled thousands of times a frame" with `watch_record`, `flan_dev_watch_reset`, `flan_dev_watch_num_i64`, `flan_dev_watch_num_f64`, `watch_num_str`, `watch_render_num`; `flan_dev_watch_read` renders a num slot instead of copying its (unused) text | working |
-| `vendor/agent/flan_agent.c` | `flan_dev_watch_reset` declaration; a `watch reset` command beside `watch on`/`watch off` | working |
-| `lib/dev.ml` | `watch_read` takes `~reset` and sends `watch reset` *after* the read; the `:op "watch"` arm reads a `:reset` field the same way `watch-enable` reads `:on` | working |
-| `emacs/flan-watch.el` | `flan-watch-ghost-call-regexp` `?` → `*`; docstring updated; `flan-watch--tick` sends `(:op "watch" :reset t)` | working |
-| `test/programs/dev-watch.flan` | `declare-c watch-num-i64`; a `loop-cells` hot loop sampling `"cell"` eight times a step at 0,3,…,21 | working |
-| `test/test_dev.ml` | assertions inside the existing watch block: the row exists, `n > 8` (per-sample not per-step), `min < max` (a range, not just `last`), a plain read does not reset, `:reset t` reopens the window | working |
-| `BUILT.md` | new section "A hot loop keeps five numbers, and the window is the editor's", after the ghost text one | working |
-| `PORTING.md` | item 5 struck in house style with the accumulator decision | working |
-| `NEXT.md` | "What is left on `PORTING.md`'s list" corrected: item 5 done, item 6 is next | working |
-
-### Verified end to end, not just compiled
-
-Against a real `flan dev` daemon and the real running program, the `"cell"` row read back:
-
-```
-n=184 min=0 max=21 last=21 mean=10.5000 (first read)
-n=368 min=0 max=21 last=21 mean=10.5000 (second read — a plain read does not reset)
-n=16 min=0 max=21 last=21 mean=10.5000 (after :reset t — the window reopened)
-```
-
-The loop is 0,3,…,21, so `min`, `max`, `mean` and the per-step count of 8 are each pinned by a different part of it.
-
-### Test result
-
-`dune test` is **green**, run twice in full. `test_dev.ml`'s first block — the separately-known-flaky socket bind race
-— failed on one earlier run with "the daemon never listened" and passed on both final runs. That is exactly the
-documented flake and another agent is on it; it is not related to anything here.
-
----
-
-## The ghost text path — the brief's assumption, checked
-
-The brief said to reuse the ghost text path rather than invent a second one. **It fits, and it needed one character.**
-
-The important property is in `BUILT.md`: ghost text anchors on the **string literal in the source**, not on a source
-location carried in the table. `flan-watch--ghost-sites` scans buffers shown in a window for
-`(
""` and matches the name against the rows. So a new *kind* of watch needs nothing new in the daemon,
-nothing new in the wire, and no new display path — the accumulator's rendered value is a string like
-`n=368 min=0 max=21 last=21 mean=10.5` and flows through the existing `:watch` rows untouched.
-
-**Is it pull-based?** No, and the brief's phrasing is worth correcting for whoever reads this next. The watch is
-**push-based** and that is load-bearing: `watch.clj` is pull (Emacs polls a render function), and `flan_dev.c`'s
-comment explains at length why that does not port — an evaluation here compiles a module and `dlopen`s it, so a 5Hz
-poll would be hundreds of `.so` files a minute. So the program pushes into a table and the editor reads *memory*.
-Ghost text is not a poller at all: it is painted from `flan-watch--absorb`, off the same single reply that paints the
-buffer, which is what keeps the two from disagreeing and preserves the one-request-in-flight invariant. The
-accumulator sits inside that unchanged.
-
-**The one thing that did not fit.** `flan-watch-ghost-call-regexp` was `watch\(?:-[[:alnum:]]+\)?` — *one* optional
-hyphenated segment. `[[:alnum:]]` does not match a hyphen, so on `(watch-num-i64 "cell" ...` the regexp matched
-`watch-num`, then required whitespace, found `-i64`, and backtracked to failure. A numeric watch therefore appeared
-normally in the watch buffer and got **no inline value at all** — a silent half-failure. `*` for `?` is the whole
-fix, verified in `emacs --batch` against all four head shapes.
-
-That is worth recording as the predictable cost of the decision the ghost text section defends. Anchoring on the name
-rather than on the head is what makes the head a `defcustom`, and a `defcustom` with an enumerated default is a
-default that needs widening whenever the set of heads grows. Cheaper than the alternative, but not free.
-
----
-
-## What remains
-
-Item 5 is complete, and so is the fixture the previous list asked for: `emacs/test-flan-watch.el` now has a block
-"Every head the default regexp claims to cover" pinning `watch-num-i64` and `watch-num-f64` beside the one-segment
-heads. It was verified by mutation — restoring the old `?\=` in `flan-watch-ghost-call-regexp` makes exactly those
-two checks fail and nothing else — so a regression in that regexp is now caught.
-
-One item is left, and it turned into a finding rather than a test.
-
-1. **`watch_render_num`'s `n=0` branch is unreachable, so the test that was asked for cannot be written.**
- `runtime/flan_dev.c:764` renders `n=0 last=` for a window with no samples. Nothing can present that state to
- the reader:
-
- - `s->num` is set to 1 in exactly one place, `watch_record` (flan_dev.c:701), and that function always falls
- through to `s->n += 1`. The clear (`s->n = 0` on an epoch change) and the increment are both inside one
- odd-generation window, so a reader cannot see between them.
- - `flan_dev_watch_begin` sets `s->num = 0` and never touches `s->n`, so the scalar path cannot leave a num slot
- at zero either.
- - `flan_dev_watch_read` never compares `s->epoch` to `watch_epoch`. A reset therefore does not make the slot read
- as empty.
-
- The user-visible consequence is worth stating on its own, because it is the thing the branch was presumably
- written for: **after `:reset t`, a read taken before the program's next sample reports the *previous* window's
- five numbers, not an empty one.** That is why `test_dev.ml`'s reset assertion has to `await` the count dropping
- rather than read once — the test is already working around this.
-
- Two ways out, and both are decisions rather than test work, which is why neither was taken here (the lane was
- scoped to tests):
-
- - **Delete the branch.** Honest about what the design says: a window begins when the program next runs, and until
- then the last window is what there is to show.
- - **Make the read epoch-aware** — compare `s->epoch` with `watch_epoch` under the seqlock and render `n=0` when
- they differ. This makes a reset visible immediately without the reader writing the table, and would let the
- existing `await` in the test become a single read. It is a behaviour change to what an editor sees in the tick
- after a reset, so it belongs to whoever owns `runtime/flan_dev.c`, not to a test pass.
-
-Item 6 (frame rollback as a worked example) is the next real piece of work and `NEXT.md` points at it.
-
----
-
-## What I tried that did not work
-
-- **Two Flan type errors in `dev-watch.flan`.** `(* i 3)` over a `let`-bound `0` is `i32`, and `watch-num-i64` wants
- `i64` — fixed with an explicit `(i64 …)`. Then `loop-cells` was declared `i64` and returned the `i32` counter;
- declared `i32`. Both are ordinary, but they cost two daemon round trips to find because a build error goes to the
- daemon's stdout, not to the client — worth knowing if you are driving `flan dev` by hand.
-
-- **My first probe script spoke the wrong protocol.** `Wire.send` is **length-prefixed** (`"%d\n%s"`), not
- newline-delimited. A newline-delimited client gets the connection closed with no error text. If you write a
- throwaway client, read `lib/wire.ml:32` first.
-
-- **Two attempts at the reset assertion failed before the third stuck**, and both failures were informative rather
- than noise:
- 1. *"the window did not reopen: 8 after 8"* — one step writes eight samples, so two reads back to back leave no
- room *under* the count for a reset to show in. The test now runs the accumulator up past 32 first (which is
- itself the other half of the claim: every one of those polls is a plain read, and a plain read must not reset,
- or the count could never climb).
- 2. *"the window did not reopen: 40 after 40"* — reading immediately after `:reset t` still sees the old count,
- because the reset bumps an epoch and the **slot clears on its next sample**. That is the design and not a bug;
- the test now `await`s the drop and the comment explains why the laziness is what keeps the reader out of the
- table.
-
-- **Not attempted, deliberately:** the `(watch "hp" hp)` arm in `check.ml`. Both `PORTING.md` and `flan_dev.c` say
- it is only wanted for *composites* (a struct, a slice, a union), which need `Render.render` pointed at the
- `flan_dev_watch_emit_*` emitters. A scalar and an accumulator both reach the runtime by plain `declare-c` and need
- no compiler change, which is the property that kept this change out of `check.ml` entirely.
-
-- **One worktree note.** This worktree was checked out at `2c232dd`, an ancient commit, rather than at `dev-loop`'s
- tip — none of `PORTING.md`, `NEXT.md`, `lib/dev.ml` or `runtime/` existed in it. `git reset --hard dev-loop` on a
- clean tree fixed it. Also, `dune` finds the parent repo's root from inside a worktree, so every build here is
- `dune build --root .` / `dune test --root .`.
diff --git a/NEXT.md b/NEXT.md
index 818e2c5..dc01a8f 100644
--- a/NEXT.md
+++ b/NEXT.md
@@ -279,7 +279,7 @@ wait a minute now, bounded by the watchdog, with a named `listening` helper in `
re-derive from `HANDOFF-f1.md` is its "lengthening any timeout" bullet — that applies to the `agent.sock` check
only, and reading it as a rule is what kept this open.
-### From `HANDOFF-f3.md` — the watch is built; two tests are not
+### ~~From `HANDOFF-f3.md` — the watch is built; two tests are not~~
`spy-num` landed and `PORTING.md` Tier 1 item 5 is closed. The `spy` half already existed — the finding that
reframed the task is that the table, the agent commands, the buffer and the ghost text were all there at `344e571`,
@@ -291,12 +291,12 @@ writes the table.
The fixture is in: `emacs/test-flan-watch.el` pins both accumulator heads beside the one-segment ones, verified by
putting the old `?` back in `flan-watch-ghost-call-regexp` and watching exactly those two checks fail.
-1. **`watch_render_num`'s `n=0` branch is dead, and the choice is whose it is to remove.** No test can reach it:
- `s->num` is set only in `watch_record`, which always falls through to `s->n += 1` inside one odd-generation
- window, and the reader never compares the slot's epoch to the global one. The live consequence is that a read
- after `:reset t` and before the program's next sample reports the *previous* window — which is why the test waits
- for the count to drop instead of reading once. Either delete the branch or make the read epoch-aware; both are
- `runtime/flan_dev.c` decisions. `HANDOFF-f3.md` has the argument.
+**Closed.** The dead `n=0` branch in `watch_render_num` is deleted and the reader stays epoch-blind on purpose: a
+stopped program does not sample, so an epoch-aware reader would blank the watch for the whole of a break loop, which
+is exactly when the numbers are being read. What was actually wrong was in the editor — `flan-watch--tick` sent
+`:reset t` five times a second at a program that could not answer it — and it is now guarded on `flan-dev--stopped`,
+with the read still going out every tick. `emacs/test-flan-watch.el` pins both halves. `HANDOFF-f3.md` is deleted;
+the reasoning is in `BUILT.md`, "A hot loop keeps five numbers, and the window is the editor's".
### Not started: five more raylib core examples
diff --git a/emacs/flan-watch.el b/emacs/flan-watch.el
index 029d7db..e3ccd97 100644
--- a/emacs/flan-watch.el
+++ b/emacs/flan-watch.el
@@ -395,7 +395,23 @@ sends the next question, leaving at most one request in flight — the invariant
;; show a number while you drag the mouse. The reset happens after
;; the read, on the daemon's side, so this tick's numbers are the
;; last tick's window and nothing is lost between the two.
- (progn (flan-dev--send flan-dev--connection '(:op "watch" :reset t))
+ ;;
+ ;; Except while the program is stopped, when the read still happens
+ ;; and the reset does not. A stopped program takes no samples, so
+ ;; there is no window for a reset to close and none for it to open:
+ ;; the runtime clears a slot lazily, on its next sample, which is
+ ;; what keeps a paused program showing the numbers from the moment
+ ;; you paused it — the whole reason to pause. Resetting anyway
+ ;; would ask for a new window five times a second that nothing can
+ ;; fill. The guard lives here rather than in the runtime because
+ ;; "since you last looked" is the editor's policy, not the table's.
+ ;; `flan-dev--stopped' is the one place that state is tracked, and
+ ;; `flan-dev.el''s background poll keeps it current whether or not
+ ;; anyone is evaluating.
+ (progn (flan-dev--send flan-dev--connection
+ (if flan-dev--stopped
+ '(:op "watch")
+ '(:op "watch" :reset t)))
(setq flan-watch--pending t))
(error (flan-watch-stop)))))))
diff --git a/emacs/test-flan-watch.el b/emacs/test-flan-watch.el
index 5b33c68..f2e18a9 100644
--- a/emacs/test-flan-watch.el
+++ b/emacs/test-flan-watch.el
@@ -210,5 +210,54 @@ what bounds its cost; a `with-temp-buffer' would be scanned by nothing."
(test-flan--check "and the last consumer out disarms the table"
(and (null flan-watch--consumers) torn))))
+;; --- The reset is guarded on the stop ------------------------------------
+;;
+;; `flan-watch--tick' asks for "since you last looked" by sending `:reset t'
+;; beside the read. A stopped program takes no samples, so there is no window
+;; for a reset to close and none for it to open — and the runtime's lazy clear
+;; is what keeps a paused program showing the numbers from the moment you
+;; paused it. So the tick must drop `:reset' while stopped and keep reading.
+;;
+;; Asserted at the level the rest of this file works at: no daemon, no socket.
+;; The tick is a function from `flan-dev--stopped' to the form it puts on the
+;; wire, and that is the whole claim, so `flan-dev--send' and `process-live-p'
+;; are stubs. What this cannot reach is the daemon actually honouring the
+;; absent field; `test/test_dev.ml' drives a real program for that.
+
+(defun test-flan-watch--tick-form (stopped)
+ "The form `flan-watch--tick' sends with the program STOPPED or not."
+ (let ((sent nil)
+ (flan-dev--stopped stopped)
+ (flan-dev--connection 'a-process)
+ (flan-dev--busy nil)
+ (flan-watch--pending nil)
+ ;; Not `buffer': that consumer checks for a live watch buffer first and
+ ;; would drop the subscription instead of ticking.
+ (flan-watch--consumers '(ghost)))
+ (cl-letf (((symbol-function 'process-live-p) (lambda (_) t))
+ ((symbol-function 'flan-dev--take-reply) (lambda (_) nil))
+ ((symbol-function 'flan-dev--send)
+ (lambda (_proc form) (setq sent form))))
+ (flan-watch--tick)
+ (list sent flan-watch--pending))))
+
+(let ((running (test-flan-watch--tick-form nil))
+ (stopped (test-flan-watch--tick-form "BoundsError")))
+ (test-flan--check
+ "a tick while the program runs asks for the window it is closing"
+ (equal (nth 0 running) '(:op "watch" :reset t)))
+ (test-flan--check
+ "a tick while the program is stopped sends no reset"
+ (null (plist-get (nth 0 stopped) :reset)))
+ ;; Skipping the tick outright would be worse than resetting: the watch would
+ ;; freeze at whatever it held when the program stopped, and a break loop is
+ ;; exactly when the numbers are being read.
+ (test-flan--check
+ "but it still reads the table"
+ (equal (plist-get (nth 0 stopped) :op) "watch"))
+ (test-flan--check
+ "and still has a reply in flight, so the cycle survives the pause"
+ (and (nth 1 running) (nth 1 stopped))))
+
(provide 'test-flan-watch)
;;; test-flan-watch.el ends here
diff --git a/runtime/flan_dev.c b/runtime/flan_dev.c
index 3c439ea..4a7ddb3 100644
--- a/runtime/flan_dev.c
+++ b/runtime/flan_dev.c
@@ -755,26 +755,35 @@ static void watch_num_str(char *out, size_t cap, double d) {
* slot, so nothing here races and nothing here runs per sample. Comfortably
* inside [WATCH_VAL]: five numbers at %.4f and their labels is well under 192
* bytes, and [snprintf] truncates rather than overruns if a caller's buffer is
- * smaller than [flan_dev_watch_val_cap]. */
+ * smaller than [flan_dev_watch_val_cap].
+ *
+ * **[n] is at least 1 whenever this runs**, so there is no empty-window arm
+ * and [sum / n] cannot divide by zero. A slot only becomes a num slot inside
+ * [watch_record], which always falls through to [s->n += 1], and the clear on
+ * an epoch change sits in the same odd-generation window as that increment, so
+ * no reader can catch a num slot at zero.
+ *
+ * There used to be an "n=0 last=..." arm here, for the window a reset opens
+ * before the program's next sample. It is deleted rather than kept, because
+ * the only way to reach it is to compare [s->epoch] with [watch_epoch] in
+ * [flan_dev_watch_read] — and that is the one thing the reader must not do. A
+ * stopped program does not sample. An epoch-aware reader would blank the watch
+ * for as long as the program stayed stopped, and looking at the numbers from
+ * the moment you stopped is the whole point of stopping. The lazy clear is
+ * right there; its cost is one stale tick in a *running* program, and the
+ * editor keeps even that honest by not sending [:reset] while the program is
+ * stopped. See [flan-watch--tick]. */
static uint64_t watch_render_num(double n, double lo, double hi, double last,
double sum, char *vd, uint64_t vcap) {
if (vcap == 0) return 0;
char b[4][40];
int k;
- if (n == 0) {
- /* No sample since the window opened. The range and the mean have nothing
- * behind them and showing the previous window's would be a lie, but [last]
- * is still the last value this name ever had, so it is kept. */
- watch_num_str(b[0], sizeof b[0], last);
- k = snprintf(vd, (size_t)vcap, "n=0 last=%s", b[0]);
- } else {
- watch_num_str(b[0], sizeof b[0], lo);
- watch_num_str(b[1], sizeof b[1], hi);
- watch_num_str(b[2], sizeof b[2], last);
- watch_num_str(b[3], sizeof b[3], sum / n);
- k = snprintf(vd, (size_t)vcap, "n=%lld min=%s max=%s last=%s mean=%s",
- (long long)n, b[0], b[1], b[2], b[3]);
- }
+ watch_num_str(b[0], sizeof b[0], lo);
+ watch_num_str(b[1], sizeof b[1], hi);
+ watch_num_str(b[2], sizeof b[2], last);
+ watch_num_str(b[3], sizeof b[3], sum / n);
+ k = snprintf(vd, (size_t)vcap, "n=%lld min=%s max=%s last=%s mean=%s",
+ (long long)n, b[0], b[1], b[2], b[3]);
if (k < 0) return 0;
if ((uint64_t)k > vcap - 1) return vcap - 1;
return (uint64_t)k;