Conversation
Problem `reasonix serve` lists sessions-v4 sessions in the Web UI sidebar by their 32-hex session id. Two independent defects produce that: 1. The v4 listing copied `info.Title` straight from the event-log projection. That field is non-empty only when a title was explicitly recorded (a manual rename, or the agent's title tool), so a session that was never explicitly named had no display title at all. 2. Serve rooted `.session-titles.json` at `ctrl.SessionDir()` - the host's legacy transcript catalog - while v4 sessions and the generated titles that describe them live in the sibling `sessions-v4` store. The cache was therefore loaded from a path that usually does not exist and was empty every run, so a title a previous Serve generated was never reusable. Both are the same failure class as the open CLI resume reports: a surface still reading the legacy layout after session writes moved to the v4 store. Fix - `titleCacheDir` maps the legacy catalog to the store root through `session.RootForLegacyDir`. - `sessionDisplayTitle` resolves each stored row in one place: explicit event-log title, then generated title, then the first authored message. The session id keys the local generation cache and the catalog preview stands in for the first message, because a v4 session has no transcript path. - A generated title is written through to the session's own event log with `SetTitleIfSequence`, guarded by the revision the listing read, so a manual rename racing the generation wins. The write is bounded and best-effort: a session owned by another runtime keeps its title and the cache serves that poll. This removes the second, non-durable owner a generated title used to have, which is why the CLI resume picker and Serve could previously name the same session differently. - v4 rows report the durable log mtime (`UpdatedAt`) rather than `CreatedAt`, so newest-first ordering is real. Verification - go test ./internal/serve/ ./internal/session/ ./internal/control/ ./internal/cli/ - go run ./tools/repolint; golangci-lint run --timeout=5m ./internal/serve/... - Mutation-checked: reverting the cache root fails TestSessionsReturnsStoredV4Title and TestSessionsKeepsLegacyTitlesAfterCacheMove; disabling the write-through fails TestGeneratedTitleBecomesDurable. - End-to-end on a local serve (isolated REASONIX_HOME, mock provider): after one /sessions poll both sessions carry a session/title event, and with .session-titles.json deleted a restarted Serve still lists both titles.
Session titles accumulated hidden context that only exists in review threads: a session has a store identity (the immutable v4 id) and a display name, the two were the same value before the v4 cutover, and every surface that renders the id is a surface that has not been taught the difference. Write the model down so it is not re-derived per fix: - the four-rung resolution ladder (explicit event-log title, generated title, first authored message, store id as last resort) and what each rung may assume during the catalog rebuild window; - why a generated title must have exactly one durable owner, and why the JSON cache must stay disposable - deleting it should cost generation requests, never information; - the migration gap: the v4 import bridge deletes legacy sources while BranchMeta.CustomTitle and TopicTitle both land empty, and import_resolver.go classifies session/title as not meaningful, so any fix there must revisit both.
|
Reissued from #10536 so the change set reads as one story instead of a read-path fix that grew Summary of how the scope grew, since the commits are new: the reported symptom was a missing |
Summary
reasonix serveshows sessions-v4 sessions in the sidebar as their 32-hex session id.Fixing it surfaced a second, larger fault on the same line, so this PR covers all three.
Defect 1 - the listing had no display title to show. For a v4 session it copied
info.Titlestraight from the event-log projection. That field is non-empty only when atitle was explicitly recorded (a manual rename, or the agent's
set_session_titletool), soa session that was never explicitly named had no display title at all.
Defect 2 - the generated-title cache was misplaced. Serve rooted
.session-titles.jsonatctrl.SessionDir()- the host's legacy transcript catalog -while v4 sessions and the titles that describe them live in the sibling
sessions-v4store. In our reproduction the loaded path was
.../sessions/.session-titles.jsonwhile thefile sat in
.../sessions-v4/, so every v4 read saw an empty cache and a title a previousServe process generated was never reusable.
Defect 3 - a generated title had two owners. Even with the path fixed, Serve kept
generated titles in a machine-local file while the store kept durable
session/titleevents. That file is not a cache of the durable title; it is a parallel title store. The
CLI resume picker reads
SessionInfo.Titleand the preview and never opens it, so the samesession could carry different names depending on which surface asked, and a migration could
drop one of them.
Changes
titleCacheDirmaps the legacy catalog to the store root throughsession.RootForLegacyDir.sessionDisplayTitleresolves every stored row in one place: explicit event-log title,then generated title, then the first authored message. The session id keys the local
generation cache and the catalog preview stands in for the first message, because a v4
session has no transcript path.
SetTitleIfSequence, guarded by the revision the listing read, so a manual rename racingthe generation wins. The write is bounded (2s) and best-effort: a session owned by another
runtime keeps its title and the cache serves that poll.
UpdatedAt) rather thanCreatedAt, sonewest-first ordering is real.
Design
docs/SESSION_TITLE_OWNERSHIP.mdrecords the model rather than leaving it in reviewthreads: store identity versus display name, the four-rung resolution ladder and what each
rung may assume during the catalog rebuild window, why the cache must stay disposable, and
the migration gap described below.
Two questions I would rather ask than assume:
listing poll can write session state. The shape I would argue for is generating on turn
completion (or explicit rename) so a listing is a pure read - that needs a decision about
where the work runs. The write-through is bounded in the meantime: once it succeeds the
durable title exists so it does not repeat, and it is revision-guarded and best-effort
against another writer's lease.
a
display_titleresolved by the session read model would let every transport render thesame ladder without re-deriving it. Out of scope here, but it is the natural next step.
Out of scope (found while investigating, belongs elsewhere)
The migration bridge in #10515 imports legacy sessions and deletes the sources, but carries
no title:
BranchMeta.CustomTitleandTopicTitleboth reach the v4 store empty whilethe sidecar holding them is deleted. Verified on the JSONL path; the sessions-v3 path has no
title handling either.
migration-map.jsonrecordssourcePathtotargetId, which isenough to re-key them. Any fix there must also revisit
internal/session/import_resolver.go,where
session/titleis grouped withsession/configanddiagnosticas not meaningful -a migrated title would otherwise be discarded as noise on re-import.
The legacy
.jsonlscan in this handler, and the test guarding it, become dead code oncethat migration deletes its sources.
Issues
Fixes #10533
Verification
go test ./internal/serve/ ./internal/session/ ./internal/control/ ./internal/cli/- pass.go run ./tools/repolint- clean (1234 baselined findings);golangci-lint run --timeout=5m ./internal/serve/...- 0 issues.TestSessionsReturnsStoredV4TitleandTestSessionsKeepsLegacyTitlesAfterCacheMove;disabling the write-through fails
TestGeneratedTitleBecomesDurable. Each guarddiscriminates its fix from its bug.
title with the local cache deleted; a rename landing between read and write survives
(
TestGeneratedTitleWriteRespectsConcurrentRename); a session held by another writer stilllists correctly (
TestGeneratedTitleWriteFailureKeepsListing).reasonix serve(isolatedREASONIX_HOME, mock OpenAI-compatibleprovider): before,
/sessionsreturned only hex ids for v4 rows; after, a stored title isreturned and an untitled session falls back to its first message. After one poll both
sessions carry a
session/titleevent, and with.session-titles.jsondeleted arestarted Serve still lists both titles - they now come from the store.
Documentation impact
Documentation-impact: updated -
docs/SESSION_TITLE_OWNERSHIP.mddocuments session titleownership, the resolution ladder, and the migration gap.
GET /sessionskeeps its wire shape.Cache impact
Cache-impact: none - the provider-visible prompt and tool schemas are untouched; the title
generation call is a separate bounded flash request. This change adds one durable
session/titleevent per session that first generates a title, which forbids prefix reusefor that session only after the appended event.
Cache-guard:
go test ./internal/serve/ -run 'TestTitleCacheDirIsTheStoreRootBesideLegacySessions|TestSessionsReturnsStoredV4Title|TestSessionsKeepsLegacyTitlesAfterCacheMove|TestGeneratedTitleBecomesDurable|TestGeneratedTitleWriteRespectsConcurrentRename'System-prompt-review: N/A