Files
box/docs/UUID-ROTATION.md
T

135 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Thread-UUID rotation: measurement & mapping hygiene proposal
> **Box is the main surface.** All operator work goes through Box (box.muse-dev.online). The web UI, `box` CLI, and agents share the same API endpoints. No UI-only powers.
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.