# Side-Effects Review — Threadline sessionComplete UUID fallback

**Version / slug:** `1.3.120`
**Date:** 2026-05-30
**Author:** `instar-codey`
**Second-pass reviewer:** `echo`

## Summary of the change

This change fixes the real production binding shape for Threadline worker
completion. `ThreadResumeMap` gains a side-effect-free reverse lookup by
`sessionUuid`, `ThreadlineRouter.onSessionComplete()` unions the existing
bound-session-name lookup with the UUID lookup, and `src/commands/server.ts`
passes `session.id` from the `SessionManager.sessionComplete` event. The touched
decision point is Threadline lifecycle demotion: whether a completed worker
session should move a conversation out of the active set.

## Decision-point inventory

- `ThreadlineRouter.onSessionComplete()` — modify — demotes matching active
  conversations when a worker session completes, now matching by name or UUID.
- `server.ts sessionComplete listener` — modify — forwards the SessionManager
  session id so the router can match the value persisted during inbound spawn.
- `ThreadResumeMap.getBySessionUuid()` — add — lookup primitive only; no direct
  lifecycle authority and no stale-retirement side effect.

---

## 1. Over-block

No block/allow surface. The closest equivalent is over-demotion: a live
conversation could be moved to `idle` if a different completed session reuses
the same UUID. SessionManager ids are unique per session, and the lookup is
limited to active conversation records, so the expected legitimate-input impact
is none. Awaiting-reply conversations are still explicitly skipped.

---

## 2. Under-block

The change still misses any conversation record that has neither a matching
`boundSessionName` nor a matching `sessionUuid`. That would require an earlier
write path to omit both identifiers. It also does not retroactively repair
already-active records written with the old logical short name and no UUID, but
the observed live path did persist `sessionUuid = session.id`.

---

## 3. Level-of-abstraction fit

This is at the lifecycle-router layer, which is the same layer that already
owns `onSessionEnd()` demotion. The lower-level resume map only exposes a lookup
primitive; it does not decide what to demote. The server listener remains a thin
event adapter from SessionManager shape to Threadline router shape.

---

## 4. Signal vs authority compliance

**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)

- [x] No — this change has no block/allow surface.

This is not a brittle detector holding blocking authority over user or agent
messages. It is lifecycle state reconciliation after SessionManager has already
emitted an authoritative completion event. The router's authority is limited to
moving matching conversation records from active to idle, with the existing
awaiting-reply guard preserved.

---

## 5. Interactions

- **Shadowing:** the UUID lookup complements the existing bound-session-name
  lookup; it does not replace it. Old records that match by name still demote.
- **Double-fire:** matches are deduped by `threadId`, so a record matching both
  name and UUID is demoted once.
- **Races:** both reverse lookups read `ConversationStore.listActive()` directly
  and do not trigger stale retirement. The final write still goes through
  `onSessionEnd()`, preserving the existing persistence path.
- **Feedback loops:** no new loop. Completion can only demote local state; it
  does not send Threadline messages or spawn sessions.

---

## 6. External surfaces

Other local agents see more accurate Threadline active-thread state after a
worker completes. The log line added in v1.3.119 now has a real chance to report
`demoted 1` for inbound Threadline spawns whose bound name differs from the
tmux/session name. Persistent `conversations.json` records may have
`boundSessionName` updated to the actual completed tmux session name during
demotion; no schema migration is required.

---

## 7. Rollback cost

Rollback is a hot-fix release reverting the UUID lookup and server listener
argument. No data migration is required. Records already demoted to `idle` would
remain idle, which is the same terminal state the stale-retirement backstop would
eventually have reached for completed workers.

## Conclusion

The live canary found a genuine integration mismatch that the original mocked
tests could not catch. The fix keeps the original name-based path, adds the
SessionManager UUID path the real runtime needs, and preserves the
awaiting-reply and no-unrelated-archiving protections. Clear to ship.

---

## Second-pass review

**Reviewer:** `echo`
**Independent read of the artifact:** `concern incorporated`

Echo specifically called out the integration risk that `boundSessionName` might
not equal the `sessionComplete` event's name (`session.name` vs `tmuxSession`) and
asked for a real post-deploy canary. That canary reproduced the mismatch. This
patch is the resolution: match by persisted SessionManager UUID as well as name.

---

## Evidence pointers

- `npm test -- tests/unit/threadline/ThreadlineRouter.test.ts tests/unit/threadline/ThreadResumeMap.test.ts` — 78/78 passed.
- `npm run build` — passed; local signing key absent, lockfile signing skipped as documented transitional state.
- `.husky/pre-push` before commit ran the scoped Threadline e2e suite — 331/331 passed.
