Code › tail-villain
Why Active Sessions Had to Resume
Treating active sessions as resumable work while preserving truthful progress
An active study session opened like an old report.
At first, that looked like a small routing bug. A history card had the wrong destination, so the fix seemed obvious: point active items back to the execution screen instead of the report page. But the more I traced it, the less it looked like a link problem. It was really about what the product meant by session state.
Active is not history. It is unfinished work.
A Tail Villain session is heavier than a chat room. The user chooses a topic, talks with a coach or villain, accumulates state, and eventually gets a report. That means history is not just a list of past conversations. It has to separate what should be resumed from what should be reviewed.
That boundary had been inconsistent. Some interview flows reused an existing active session. Other topic-level flows abandoned the old session and created a new one when the user started again. To the user, both paths looked like session history. To the backend, they meant different things.
The worst part was the abandoned state itself. It was not something the user chose. It was an artifact of replacement behavior.
If the user did not abandon a session, the product should not make it look abandoned.
So I moved the behavior toward resumable active lanes. When the same user returns to the same learning target and an active lane already exists, the backend should return that session instead of silently replacing it. I also added database-level uniqueness around active interview and study sessions, then made startup flows recover from uniqueness conflicts by reading back the winning active session.
That was not only a duplicate-session fix. It was a product promise: if the user leaves and comes back, Tail Villain remembers where they were.
The history UI had to follow the same rule.
A completed session should open a report. The conversation is finished, and the useful artifact is the result. An active session should not open a report, because there is no final result yet. The useful destination is the place where the user can keep talking.
On April 25, I changed active study-history items so they route directly back to the running session. I also added a server-side guard for the report page. If a live session URL reaches the report surface, the page checks the status and redirects before rendering the body, avoiding the awkward flash where a report skeleton appears for something that is not actually a report.
That sounds small, but it changes the user’s read of the product. If an active card resumes the conversation, the product feels like it is preserving the user’s current work. If an active card opens a report, the product treats unfinished work like an archive.
Progress needed the same honesty.
There was logic that forced every milestone to 100% when a study session completed. It made the report look clean. A completed session got a completed-looking result, and the bars filled up.
But that is dangerous generosity in a learning product.
Finishing a conversation does not mean the learner mastered every milestone. Some parts may have been answered well. Other parts may still be shaky. Session completion is the end of a conversation, not automatic proof that every learning goal has been conquered.
So I changed completion reports to preserve the actual progress accumulated during the conversation. The product can say the session ended without pretending every milestone is solved. If a topic is really at 60%, the next review should know that. Otherwise, the spaced-review loop starts from a flattering lie.
Truthful progress may feel less satisfying in the moment, but it gives the next session something real to work with.
The dashboard work from April 22 was connected to the same idea. The dashboard was moving away from a plain list and toward a denser view that made the next action easier to see. Button labels also had to reflect session state. Starting something and continuing something are not the same action.
The point was not to make the UI busier. The words on the screen had to match the backend state. Active should not look archived. Completed should not look resumable. Roadmap cards, history modals, session pages, and report pages all needed to share the same semantics.
The study coach prompt needed a similar cleanup. A session can technically resume while the conversation still drifts. If the coach repeats itself or ends with soft permission-seeking closers, the user stays inside the screen but loses direction. I tightened the prompt so follow-up questions include a brief reason and move toward a concrete next question, example, or task.
Resuming a session is not just restoring a URL. The conversation has to keep its direction too.
Looking back, this was not a history UI cleanup. It was a cleanup of time inside the product.
Active is present tense. Completed is past tense. Abandoned was mostly an implementation scar, not a user-facing concept. A forced 100% progress report was optimism generated by the product, not evidence from the session.
An AI learning product should encourage users, but it should not decorate state until it becomes false. It needs to show what is still running, what is really finished, and what the learner has actually strengthened.
That is why active sessions had to resume. The product needed to remember the user’s time accurately.