Review fixes for #386.
Duplicate rows. A codex conversation showed twice, once live and once as a
past rollout row, because nothing aliased a codex session to its thread id.
That is worse than cosmetic: the stale row still resumes, so clicking it
starts a second `codex resume` on a thread already open in another pane.
- A RESUMED session knows its thread id up front, so it folds from its own
side: add `codexConfig.resumeSessionId` to the `claudeSessionId` chain.
Not only in the constructor — `start()` recomputes that id at two further
points (the mux branch, and the unconditional "third reset point" whose
own comment already warned that omitting omp's fallback there stomps the
mux branch's resolved alias). Both listed Claude's and omp's ids only, so
for codex every mux reattach and boot recovery reset the alias back to
the Codeman id and the duplicate returned.
- A FRESH session has no thread id until codex writes the rollout, so it is
folded from the other side. The scanner now reports
`session_meta.originator`, which is `codeman_<sessionId>` for every pane
Codeman spawns, and `gatherUnifiedInputs()` stamps the matching live and
persisted rows, newest rollout winning (`/new` inside the TUI leaves
several rollouts sharing one originator).
- Persisted rows read `codexConfig.resumeSessionId` too. A resumed session
demoted to a persisted-only record would otherwise lose its alias, and
the originator fallback cannot rescue that one: a resumed rollout keeps
its ORIGINAL session_meta, so it still names the pane that created the
thread rather than the pane that resumed it.
Identity cache. It was written as soon as the thread id was known, but codex
writes the first user message only when the user submits, so any scan in that
window pinned `firstPrompt: undefined` for the life of the process — and the
home screen, the command palette and the search-index refresh all scan.
`shouldCacheIdentity()` now keeps an identity only once the prompt is known or
the head read filled its whole window.
Also from review: both caps count emitted rows rather than file index, so a
store of sub-agent threads no longer spends the `lastPrompt` budget before the
first row that needed it; the cache is an `LRUMap` sized like the one beside
it; the unreachable filename fallback is gone; a rollout recording no cwd is
dropped rather than emitted with `workingDir: ''`; and the unified-session
module header names all three transcript stores.
Tests. The resume wiring now has cases for a row with a thread id, a row
without one, and a `resumeId` on a non-codex row; the "no continuation is
wired" case narrows to gemini/antigravity, which is no longer true of codex.
`codex-resume-alias-survives-start.test.ts` drives a real Session through
`start()` rather than asserting on pre-stamped inputs — that gap is why the
reset points went unnoticed. Plus the maintainer's own cache repro, the
tail-budget case, a no-cwd case, and merge cases for both folds.
resumeHistorySession() creates the resumed row in its own mode via a
modeConfigKey map (opencode/pi/grok/omp -> continueSession, deepseek ->
resumeSession) and retires the old row afterward. codex, gemini and
antigravity were missing from that map, so resuming one of their rows
started a brand-new session with NO continuation while still deleting
the row it came from -- silent data loss dressed as the duplicate-row
fix. Gate row retirement on continuesSomething (true only for modes that
actually got a continuation config) instead of wiring an unverified
sessionId->native-conversation-id assumption for the three affected CLIs.
DELETE /api/sessions/:id reimplemented the ownership 404 check inline in
two places instead of going through findSessionOrFail, and its
persisted-only-session branch never broadcast session:deleted, so other
open tabs kept the retired row until their next unrelated fetch. Extract
the shared 404 into sessionNotFoundError(), add findPersistedSessionOrFail()
alongside findSessionOrFail() in route-helpers.ts (same ownership
contract, returns a SessionState instead of a live Session), and use both
from the route instead of inline checks. Add the missing broadcast.