Files
box/docs/UUID-ROTATION.md
T

133 lines
7.4 KiB
Markdown
Raw Normal View History

# Thread-UUID rotation: measurement & mapping hygiene proposal
Date: 2026-10-04. Source: `dm-log.jsonl` (window 2026-10-03 21:36Z → 2026-10-04 19:46Z,
~22h, 3318 lines — the log does not cover a full 7 days; rates below are
extrapolated from this window and should be re-measured on a longer one).
Read-only analysis; no code changed.
## 1. Measured rotation per alias
Two distinct phenomena share the symptom "alias → different UUID":
- **(A) Real thread death/rotation.** The SPA retires threads. Observed earlier
for heartbeat-opm (5bd5b350 → 0077e918 → dead, twice in one day, per operator
notes; predates this log window).
- **(B) Fuzzy-search nondeterminism.** `sidechat use "<name>"` scans every DOM
element for `textContent.includes(name)`, picks the shortest match, and clicks
it (`muse-chat-api.py: cmd_sidechat_use`). The candidate set changes between
React renders, so the same alias resolves to different live threads run to run
— with no thread dying at all.
| alias | distinct UUIDs (window) | autoprovisioned (real rotations) | send-target flaps (fuzzy) | changes/day* |
|---|---|---|---|---|
| heartbeat | 4 (5f18476d, 1e75a740, 0077e918, …) | 0 | 3 | ~3.3 flap |
| 646 tasks | 4 (1e75a740, a7d0b08a, 1dfb3199) | 1 (18:30 → 1dfb3199) | 2 | ~1.1 real, ~2.2 flap |
| 646-pip | 2 (82779e55 → 75feb3a2) | 1 (18:01) | 0 | ~1.1 real |
| ops-audit | 2 (75feb3a2 → 34a8a87c) | 1 (19:28) | 0 | ~1.1 real |
| main-loop brain | 1 (5f18476d) | 0 | — | collision, see §3 |
| pipe-* (6 aliases) | 1 each | 1 each (creation) | 0 | 0 post-creation |
| 646-pip-coord | 1 (4466d0c1) | 0 | 0 | 0 |
| 646-opm-coord | 1 (4139dd4e) | 0 | 0 | 0 |
\* changes/day extrapolated from ~22h window; demo-day traffic inflates flaps.
Smoking gun for (B): at 16:25:53 a `super`→646 send for "646 tasks" landed on
a7d0b08a, and 52 seconds later an `opm`→646 send for the same alias in the same
recipient browser landed on 1e75a740. No thread died in between — the fuzzy
search is nondeterministic across runs.
## 2. Who writes / reads job-sidechats.json today
Writers:
- `dm.py` autoprovision path only (atomic tmp+mv write of
`{thread_uuid, agent, created_at}` on `sidechat_autoprovisioned`).
- Manual edits (e.g. today's removal of stale heartbeat-opm / pipe-9735f2).
NOT writers:
- The new sweeper thread_uuid backfill (workstream 1) writes **followups.json
only** — it does not refresh job-sidechats.json.
- `thread_lifecycle.py` exists but is marked "prototype, not deployed".
Readers: `dm.py resolve_sidechat_target` (exact key match → UUID, else
passthrough to fuzzy search), harvester/siphon (monitor list), box-ctl,
job-dispatch, super-cli.
Critical gap in the refresh story: a **dead mapped UUID does not self-heal**.
`sidechat use <dead-uuid>` → SPA redirects to `/` → output is
"Navigated to: https://muse.ai/" (not NOTFOUND) → `nav_failed no_thread_url` →
hard fail. Autoprovision only fires on NOTFOUND from *name* search. So a dead
mapping = permanent send failures for that alias until a human removes the
entry. (This is fail-closed, not misdelivery — but it is not self-healing.)
Second gap: an **unmapped alias whose fuzzy search keeps "succeeding" never
autoprovision** and therefore never earns a canonical mapping — it flaps
forever. This is heartbeat's exact situation right now (see §3).
## 3. Flags
- **MISSING mapping, highest churn: `heartbeat`.** 4 UUIDs in 3h, zero
autoprovision events, no entry in job-sidechats.json (the old `heartbeat-opm`
entry was removed during today's hardcoded-alias cleanup). Every send does a
fresh fuzzy search. At 16:45 a heartbeat send landed on 1e75a740 — 646 tasks'
thread. **Misdelivery already happened here.**
- **MISSING mapping: `ops-audit`.** Re-provisioned 19:28 (34a8a87c) but the
alias was never registered; next fuzzy search may flap.
- **COLLISION, unverified: `main-loop brain` → 5f18476d.** The brain's init
send (19:40) fuzzy-matched into 5f18476d — the same thread heartbeat used at
16:35 and 19:41. No `sidechat_autoprovisioned` event for the brain alias, so
no new thread was created. Either the fuzzy search false-positived into
heartbeat's thread, or the brain was deliberately placed there. **Confirm
intent — if unintended, digests and heartbeat traffic are cross-talking.**
- **No current mapping points to a UUID superseded by a newer autoprovision
for the same alias** — all 11 entries match their alias's latest
autoprovision event. Nothing stale in that narrow sense.
- **Aging, low-risk:** `test-auto-prov` (last confirmed 16:20), `646-opm-coord`
(last confirmed 12:52), and six single-use `pipe-*` demo entries accumulate
with no TTL. Harmless today, cruft tomorrow.
- **Confounder:** as of ~19:43Z all four chromebox browsers are flapping
(dying every 10–15 min, no crash signature). Nav flakiness measured today is
polluted by this; re-measure rotation after the browsers stabilize.
## 4. Proposal: mapping hygiene
1. **Canonical entry for every recurring alias.** `heartbeat`, `ops-audit`,
`main-loop brain` (once its thread is confirmed), `646 tasks`, `646-pip`,
`646-pip-coord`, `646-opm-coord` must each have exactly one entry, created
by explicit provisioning — never by fuzzy luck. Unmapped + fuzzy-matched is
the misdelivery configuration; it should be treated as a bug, not a
fallback.
2. **Dead-UUID reprovisioning.** On `no_thread_url` / `uuid_mismatch` where
the nav target came from a *mapping* (source == `dynamic_mapping`), fall
through to autoprovision and overwrite the mapping — the same path name
search uses today. This closes the "dead mapping = permanent failure" gap
and makes rotation self-healing. (Keep failing closed when even
autoprovision can't produce a thread page.)
3. **Exact-match-first fuzzy search.** Before the DOM substring scan,
`sidechat use` should try an exact title match (normalized case/space).
Only fall back to substring on no exact hit. This kills most cross-alias
false positives (the 16:45 heartbeat→646-tasks landing) without changing
the autoprovision contract.
4. **TTL + audit job.** Add `last_confirmed_at` to each mapping entry, refreshed
on every successful send through it. A periodic audit (daily is plenty;
weekly at minimum) flags: entries unconfirmed > TTL (7d job, 24h heartbeat),
aliases seen in dm-log with no entry, and UUIDs serving >1 alias
(collision detector — would have caught 1e75a740 and 5f18476d). Report-only
at first; auto-prune single-use `pipe-*`/test entries after TTL.
5. **Sweeper backfill → also refresh the mapping.** The workstream-1 backfill
already learns the live UUID per followup; writing it through to
job-sidechats.json when the followup's target is a mapped alias is nearly
free and covers the "nudge provisioned a thread dm.py didn't" case.
6. **Do not reintroduce hardcoded UUIDs** (the empty `SIDCHAT_ALIASES` was the
right call). The mapping file is the single source of truth; the audit job
in (4) is what keeps it honest.
## 5. Suggested next steps (no code in this workstream)
- Confirm `main-loop brain` thread intent (collision with heartbeat's 5f18476d).
- Provision canonical threads for `heartbeat` and `ops-audit`; register them.
- Implement proposal items 2 and 3 (dm.py + muse-chat-api.py — small, surgical).
- Stand up the audit job (4) as report-only; review one week of flags before
enabling any auto-prune.
- Re-run this measurement on a 7-day dm-log window once rotation stabilizes
post-browser-flap.