# Side-Effects Review — Threadline completion direct demotion

**Version / slug:** `1.3.121`
**Date:** 2026-05-30
**Author:** `instar-codey`
**Second-pass reviewer:** `live canary`

## Summary of the change

This patch fixes the final write step in Threadline session-completion
retirement. The v1.3.120 UUID lookup found live inbound worker conversations,
but `ThreadlineRouter.onSessionComplete()` delegated to `onSessionEnd()`, which
re-read the thread through the resume guard. For live Threadline spawns the
stored UUID is a `SessionManager` id, not a resumable transcript UUID, so the
guard could return null. The completion log then said `demoted 1` while the
conversation stayed `active`.

The router now writes the matched entry to `idle` directly after the
awaiting-reply guard passes.

## Decision-point inventory

- `ThreadlineRouter.onSessionComplete()` — modify — demotes from the matched
  live conversation entry instead of re-entering the transcript resume guard.
- `ThreadlineRouter.onSessionEnd()` — unchanged — remains the explicit
  thread-id + resumable-UUID path.

---

## 1. Over-block

No block/allow surface. The over-demotion risk is unchanged from v1.3.120:
only active conversations already matched by bound session name or
SessionManager UUID are moved to `idle`, and `awaiting-reply` conversations are
skipped before any write.

---

## 2. Under-block

The change still cannot demote records that have neither a matching
`boundSessionName` nor a matching `sessionUuid`. The observed live path does
persist `sessionUuid`, and v1.3.120 added the lookup.

---

## 3. Level-of-abstraction fit

The fix stays in the lifecycle router, where the completion decision already
lives. `ThreadResumeMap` remains the storage adapter; it is not given new
authority to decide lifecycle state.

---

## 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.

The authoritative completion signal still comes from `SessionManager`. This
patch only corrects how the router applies that signal to a matched local
conversation.

---

## 5. Interactions

- **Resume guard:** direct demotion intentionally bypasses the transcript
  existence guard because the match already came from a live conversation
  record, not from a user-initiated resume lookup.
- **Awaiting reply:** preserved; `awaiting-reply` records are counted and left
  untouched before direct demotion runs.
- **Double match:** still deduped by `threadId` before writes.
- **Logging:** the existing `demoted N / skipped M` log now reflects actual
  writes instead of possible guard-filtered no-ops.

---

## 6. External surfaces

Other agents and dashboards should see completed Threadline worker conversations
leave the active set promptly. Existing records are not migrated; they demote
when their bound worker emits a completion event.

---

## 7. Rollback cost

Rollback is a small patch reverting `onSessionComplete()` to delegate through
`onSessionEnd()`. No schema or data migration is involved.

## Conclusion

The live canary caught a false-positive demotion log. This patch keeps the
v1.3.120 name/UUID matching and makes the matched completion path actually
persist `idle`.
