Update project configuration and documentation

ober

59e6405c4c9508559ab0f40ea7f0e588bb527e1a

diff --git a/.jpkgignore b/.jpkgignore
new file mode 100644
index 0000000..7261f4d
--- /dev/null
+++ b/.jpkgignore
@@ -0,0 +1,11 @@
+vendor/
+build/
+dist/
+.jpkg/
+target/
+node_modules/
+__pycache__/
+*.so
+*.o
+*.a
+.git/
diff --git a/AGENTS.md b/AGENTS.md
index 0f5b749..9b29031 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -371,7 +371,6 @@ improved versions of the above.
 When working in a Jerboa project, **ONLY modify files in the current repo** unless the user explicitly names another path.
 
 Common sibling repos that exist but must NOT be touched without explicit instruction:
-- `~/mine/jerboa-emacs` — **NEVER touch**. Another model owns it.
 - `~/mine/jerboa-mcp` — Legacy node MCP, superseded. The active MCP server now lives in THIS repo at `mcp/` + `data/`. Don't modify the legacy repo unless told.
 - `~/mine/jerboa-shell` — Only modify when user explicitly says to work there.
 - `~/mine/gerbil-mcp` — **NEVER touch**. Deprecated.
diff --git a/Makefile b/Makefile
index a07995e..566dce8 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 # jerbuild bundles Chez Scheme and the Jerboa stdlib. When a sibling Jerboa
 # checkout is present, use its patched jerboa-native-rs crate for the encrypted
 # log backend; otherwise fall back to the native crate bundled with jerbuild.
-JERBOA_VERSION ?= v0.2.3
+JERBOA_VERSION ?= v0.2.8
 JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin
 JERBUILD ?= $(shell if [ -x ./jerbuild ] && [ -x ./jerboa ]; then \
 	printf '%s\n' ./jerbuild; \
diff --git a/handoff-corruption.md b/handoff-corruption.md
new file mode 100644
index 0000000..085f7be
--- /dev/null
+++ b/handoff-corruption.md
@@ -0,0 +1,133 @@
+# Handoff: Fix TUI display corruption (stale cells, forced C-l)
+
+## Task
+
+Make the jerboa-signal TUI self-heal from terminal display corruption instead of
+requiring the user to press `C-l` manually.
+
+## Symptom (evidence: ~/signal.png)
+
+The screenshot shows the same 3-column layout (contacts / thread / details)
+repeated **three times, stacked vertically**, each copy scrolled to a different
+point. That is the signature of stale cells from previous frames remaining on
+the physical terminal: the TUI thinks the screen is already correct and its
+incremental repaint never touches those cells again. Pressing `C-l` (force
+full repaint) clears it — the user reports being "forced to always hit C-l".
+
+## Environment / stack
+
+Three independent incremental-diff renderers are stacked:
+
+```
+Emacs vterm  →  tmux  →  jerboa-signal (termbox2, vendored in vendor/termbox2)
+```
+
+vterm and tmux are **external to this repo — do not attempt to fix them**.
+(A `tmux-problem.md` exists in the sibling `jerboa-term` repo as background;
+do not modify that repo.) The fix must make jerboa-signal stop depending on
+its cell-diff staying in sync with the real terminal.
+
+## Root cause (verified in code)
+
+- `draw!` (`signal/tui/main.ss:2567-2609`) fully repaints termbox2's **back
+  buffer** every frame: `tb-set-clear-attrs!` + `tb-clear!` + full-screen
+  `fill-rect!` + all panels. The Scheme side is NOT the problem — every frame
+  is a complete frame.
+- `tb-present!` then emits terminal escape sequences **only for cells that
+  differ** between front and back buffers (termbox2's cell-diff optimization).
+- When tmux/vterm drops or mangles one escape sequence (cursor addressing,
+  erase, scroll region), the physical terminal diverges from termbox2's front
+  buffer. From then on the diff considers the corrupted cells "already
+  correct" and never rewrites them. A mangled cursor-home also explains the
+  "same frame repeated at a vertical offset" pattern in the screenshot.
+- The only recovery is `tb_invalidate()`, which marks every cell dirty and
+  forces a full non-differential repaint on the next `tb_present()`.
+- `tb-invalidate!` is currently called from exactly **two** sites (verified by
+  grep — there are no others):
+  1. Resize event: `signal/tui/main.ss:1384-1387` (also updates w/h in state)
+  2. `C-l` key: `signal/tui/main.ss:1390-1392` (plus sets "Screen redrawn."
+     status)
+- The event loop (`signal/tui/main.ss:397-408`) redraws only when dirty and
+  otherwise blocks in `tb-peek-event` with `*idle-poll-ms* = 250`
+  (`signal/tui/main.ss:394`). Nothing ever triggers a full repaint on a timer.
+
+So: corruption between resize events persists forever, until the user hits C-l.
+
+## Verified code map
+
+| What | Where |
+|---|---|
+| Event loop (draw-when-dirty, 250ms idle poll) | `signal/tui/main.ss:394-408` |
+| `handle-event!` — resize → `tb-invalidate!` | `signal/tui/main.ss:1382-1387` |
+| `handle-event!` — `C-l` → `tb-invalidate!` | `signal/tui/main.ss:1390-1392` |
+| `draw!` — full back-buffer repaint per frame | `signal/tui/main.ss:2567-2609` |
+| FFI wrappers `tb-clear!` / `tb-present!` / `tb-invalidate!` | `signal/tui/ffi.ss:222-224` |
+| C shim → real termbox2 `tb_invalidate()` | `signal/tui/signal_tui_shim.c:61` |
+
+## Constraints — do NOT
+
+- Do NOT remove or weaken the existing `C-l` handler or the resize-time
+  `tb-invalidate!`; they stay as-is.
+- Do NOT modify the C shim (`signal_tui_shim.c`) or anything under `vendor/`.
+- Do NOT touch the logdb/crypto code or any tests unrelated to the TUI loop.
+- Do NOT modify any sibling repository (`jerboa-term`, `jerboa-shell`, etc.).
+- Keep the change inside `signal/tui/main.ss` if possible.
+
+## Recommended fix: periodic self-healing invalidate
+
+Call `tb-invalidate!` on a wall-clock interval (e.g. every ~10 seconds) so any
+desync self-heals without user input. `C-l` remains as the instant manual
+escape hatch.
+
+Sketch, in `event-loop` (`signal/tui/main.ss:397-408`):
+
+```scheme
+(def *invalidate-interval-s* 10)  ;; force a full repaint this often
+
+(def (event-loop state actor)
+  (let loop ([dirty? #t] [last-invalidate (real-time)])
+    (let* ([actor-dirty? (handle-actor-events! state actor)]
+           [send-dirty?  (handle-send-events! state)]
+           [timer-dirty? (expire-typing-indicators! state)]
+           [now          (real-time)]
+           [invalidate?  (>= (- now last-invalidate) *invalidate-interval-s*)])
+      (when (or dirty? actor-dirty? send-dirty? timer-dirty?)
+        (when invalidate? (tb-invalidate!))   ;; <-- the fix
+        (draw! state)
+        (tb-present!))
+      (let* ([ev (tb-peek-event *idle-poll-ms*)]
+             [event-dirty? (and ev (begin (handle-event! state actor ev) #t))]
+             [last-invalidate (if invalidate? now last-invalidate)])
+        (unless (tui-state-quit? state)
+          (loop event-dirty? last-invalidate))))))
+```
+
+Notes for the implementer:
+
+- `(real-time)` is already used in this file (see
+  `expire-typing-indicators!`, line 411) — no new imports needed.
+- 10 s is a suggestion; anything in the 5–30 s range is fine. The cost of a
+  full repaint of a small TUI frame is negligible.
+- Optional cheap add-on (not required): also call `tb-invalidate!` on mode
+  transitions (chat ↔ new-message/search/attach/captcha/theme), since those
+  repaint the whole screen anyway. Only do this if it stays a 1–2 line change.
+
+## Acceptance criteria
+
+1. `make binary` succeeds (this host is macOS — do NOT run `make docker-build`;
+   that is the Linux pipeline).
+2. `make test` passes in full.
+3. Manual check: run the TUI inside tmux (ideally under Emacs vterm), resize
+   the pane repeatedly, scroll, and switch modes. Any corruption that appears
+   must clear by itself within the invalidate interval, without pressing C-l.
+4. `C-l` still forces an immediate repaint and shows "Screen redrawn.".
+5. Grep confirms `tb-invalidate!` call sites are now: resize, C-l, and the
+   new periodic one — nothing else removed.
+
+## If the edit "has no effect"
+
+Delete stale compiled artifacts and rebuild:
+
+```bash
+find lib -name "*.so" -delete && find lib -name "*.wpo" -delete && make build
+```
diff --git a/signal.md b/signal.md
new file mode 100644
index 0000000..1bfef0a
--- /dev/null
+++ b/signal.md
@@ -0,0 +1,74 @@
+╭─── Claude Code v2.1.218 ─────────────────────────────────────────────────────╮
+│                                                    │ Tips for getting        │
+│                 Welcome back Jaime!                │ started                 │
+│                                                    │ Run /init to create a … │
+│                       ▐▛███▜▌                      │ ─────────────────────── │
+│                      ▝▜█████▛▘                     │ What's new              │
+│                        ▘▘ ▝▝                       │ Changed `/code-review`… │
+│    Sonnet 5 · Claude Pro · jaimef@linbsd.org's     │ Added screen-reader an… │
+│    Organization                                    │ Fixed Windows paths wi… │
+│                ~/mine/jerboa-signal                │ /release-notes for more │
+╰──────────────────────────────────────────────────────────────────────────────╯
+
+
+❯ Please see ~/signal.md, in the jsh mux, it renders with corruption, and I'm   
+  forced to always hit C-l                                                      
+
+  Searched for 1 pattern, read 1 file, listed 1 directory (ctrl+o to expand)
+
+⏺ No signal.md exists anywhere near ~. There is a ~/signal.png modified today
+  though — likely what you meant. Let me look at it.
+
+  Read 1 file (ctrl+o to expand)
+
+⏺ No signal.png corruption is evident there (looks like a clean render) — that's
+  probably not what you meant either. There's a staged kimi3.md in this repo
+  that matches the terminal status bar in that screenshot ("6:get kimi3"). Let
+  me check that instead.
+
+  Read 1 file (ctrl+o to expand)
+
+⏺ That matches — findings A1/A2 in this doc are exactly the rendering bugs
+  (scroll arrows overwriting rows), which explains the corruption forcing C-l.
+  Let me quickly confirm there's no separate signal.md elsewhere before acting
+  on this.
+
+  Listed 1 directory (ctrl+o to expand)
+
+⏺ No signal.md anywhere — kimi3.md findings A1/A2 are the rendering bugs causing
+  the corruption you're describing. Let me look at the actual code and fix
+  them.
+
+  Read 1 file (ctrl+o to expand)
+  ⎿  Interrupted · What should Claude do instead?
+
+❯ Please see ~/signal.png, in the jsh mux, it renders with corruption, and I'm  
+  forced to always hit C-l                                                      
+
+⏺ That screenshot itself is the evidence — reading it again with
+  rendering-corruption specifically in mind: it shows the same 3-column layout
+  (contacts / thread / details) repeated three times stacked vertically, with
+  the same Larissa conversation just scrolled to different points each time.
+  That's the signature of a TUI that doesn't fully clear the screen on
+  resize/redraw — leftover panels from a previous (taller) frame stay on screen
+  underneath the new one, which is exactly what C-l (force full clear+redraw)
+  papers over. Let me check how the mux/resize handling works in the code.
+
+  Searched for 7 patterns, read 1 file, listed 2 directories (ctrl+o to expand)
+
+⏺ There's a tmux-problem.md right in jerboa-term — that's very likely
+  documenting this exact issue. Let me read it.
+
+  Read 1 file (ctrl+o to expand)
+
+⏺ This document (about tmux+vterm+TUI issues generally) confirms the stack:
+  Emacs vterm → tmux → jerboa-signal's termbox2 UI. That's three independent
+  incremental-diff renderers stacked (termbox2's cell-diff, tmux's pane-diff,
+  libvterm's grid), and any single dropped/misparsed escape sequence in that
+  chain leaves stale cells that only a full non-differential repaint clears —
+  which is exactly what C-l does today (tb-invalidate!, confirmed at
+  signal/tui/main.ss:1390-1392). I can't fix vterm or tmux from this repo, but I
+  can make jerboa-signal stop depending on the diff staying in sync. Let me
+  check the render loop.
+
+  Searched for 2 patterns, read 1 file (ctrl+o to expand)  ⎿  Interrupted · What should Claude do instead?
\ No newline at end of file