Skip to content

Session titles live on sessions; search names sessions by title; titles never contain quotes - #965

Open
ColemanDuPlessie wants to merge 11 commits into
mainfrom
session-titles-on-sessions
Open

Session titles live on sessions; search names sessions by title; titles never contain quotes#965
ColemanDuPlessie wants to merge 11 commits into
mainfrom
session-titles-on-sessions

Conversation

@ColemanDuPlessie

@ColemanDuPlessie ColemanDuPlessie commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Ports three changes made in our benchmarking repo, centered around reorganizing how we deal with session titles:

  • session_titles table remove, now is a part of sessions
  • stash search CLI command now gives session titles, rather than IDs. The underlying endpoint still returns both, since web search still gets sessions by IDs.
  • Quotes are stripped from session titles, to prevent the ugly case where we force agents to use triple-nested quotes to make the query they want (e.g. stash vfs "grep ... 'my title that "has quotes" ...' ...")

AI-generated longer summary below...

1. session_titles folded into sessions

The side table was 1:1 with sessions and FK'd to it — every reader had to LEFT JOIN it, every writer upserted against a table that couldn't exist without its sessions row. Migration 0179 adds title, title_source_hash, title_user_set, title_updated_at to sessions, carries existing titles forward in one shot, and drops the table. All consumers (title service, generation task, share service, sidebar etag) now read and write the sessions columns; set_user_title fails loud if the session row doesn't exist.

2. stash search names sessions by title

Session search hits now carry name — the session's display title in the VFS's spelling (new session_title_service.titles_for_session_ids + public stashvfs.safe_name) — so a hit can be followed straight into /sessions/<name>/. The CLI prints the title and omits the raw session id, which isn't a usable handle there.

Deliberate deviation from the benchmarking commit: it removed ref from session hits entirely. Here ref is kept — the web search page (frontend/src/app/search/page.tsx) links sessions via hit.ref and skips ref-less hits altogether, so dropping it would have removed sessions from web search. No frontend changes needed.

3. Stored titles never contain quotes

Generated, derived, and user-set titles all pass _strip_quotes (removes ', ", and backticks). Stripping happens after reply-shape detection, whose patterns need the apostrophes ("I'll …", "you're …") intact. VFS names additionally strip $ — titles become VFS directory names, which agents reach through two layers of shell parsing in stash vfs "<script>".

Testing

  • Full backend suite green on a fresh test DB: 1267 passed (one deselected: test_admin_creates_workspace_with_working_bootstrap_key, which fails identically on main — pre-existing auth0_sub env issue).
  • CLI suite: 210 passed, 1 pre-existing failure on main too (test_ls_unknown_source_fails_loudly, console-rendering assertion).
  • Migration 0179 applies cleanly from scratch and on a DB with existing titles; downgrade recreates the table and copies titles back.
  • New tests: rename strips quotes end-to-end; clean_generated_title/title_from_text strip quotes; VFS drops shell-hostile characters from session dir names.
  • ruff check and ruff format --check clean.

No GUI changes — the web search page's behavior is intentionally unchanged.

🤖 Generated with Claude Code

…es never contain quotes

Three changes, ported from the stash-benchmarking pilot (4dfd3ddc):

- Fold session_titles into sessions (migration 0179). The side table was
  1:1 with sessions and FK'd to it; every reader joined it and every
  writer upserted against it. title/title_source_hash/title_user_set/
  title_updated_at are now columns on sessions, existing titles are
  carried forward in one shot, and the table is dropped.

- Session search hits carry `name` — the session's display title in the
  VFS's spelling (new titles_for_session_ids + public stashvfs.safe_name)
  — so `stash search` prints titles, not ids, and a hit can be followed
  straight into /sessions/<name>/. Unlike the benchmarking commit, `ref`
  is kept: the web search page links sessions with it (and drops
  ref-less hits entirely).

- Stored titles never contain quotes or backticks: generated, derived,
  and user-set titles all pass _strip_quotes (after reply-shape
  detection, whose patterns need the apostrophes), and VFS names strip
  shell-hostile characters — titles become VFS directory names, which
  agents reach through two layers of shell parsing.

Co-Authored-By: Claude Fable 5 <[email protected]>
@assert-app

assert-app Bot commented Aug 3, 2026

Copy link
Copy Markdown

@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stash-web Ready Ready Preview Aug 11, 2026 6:23pm

Request Review

@henry-dowling henry-dowling left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(1) Getting rid of session_titles table: love it, great call

(2) stash search returns titles of sessions- I have a couple thoughts:

  • I believe we unintentionally affect our search rankings here! Look- now we rank based on both title and snippet, not just snippet. was this intended? if not, we should keep existing ranking behavior.
  • why hide the session ref when searching for sessions? why not include both the ref and the title? refs are nice because they're guaranteed to be unique
  • this also introduces inconsistency between our API and CLI- the API returns both, and the CLI returns only the name after this change.
  • Edge case to remember- when we have two separate files in the VFS with the same name, we'll automatically disambiguate them with a UUID. Eg
    /sessions/Fix auth--12345678
    /sessions/Fix auth--abcdef12

this is important because it means if you search just using the name of the file you're looking for, it may fail if there were two files created with this name.

(3) I think we're doing this at the wrong place- we're sanitizing titles at import- why not sanitize titles only when we're about to display them in the VFS? This will let us preserve the important invariant that if you rename a session, its name in Stash is the same name that you typed in.

Basically, the overall takeaway for (3) is- we should persist the ground truth data, not anything that we derive from it. This is so that we don't lose information (that might be important!)

Import/storage no longer strips quotes from titles: _strip_quotes is
gone from set_user_title, title_from_text, title_from_events, and
clean_generated_title, so the original spelling — user-set or generated
— is what lands in sessions.title. Shell-hostile characters (quotes,
backticks, $) are removed in exactly one place, stashvfs safe_name,
when a title is projected into a VFS path or search-hit name.

Backticks still disappear from generated titles via _strip_markdown —
that is markdown cleanup of LLM output, not shell sanitization.

Co-Authored-By: Claude Fable 5 <[email protected]>
Session hits were printing only the title, on the theory that the raw
id is not a usable handle. But the id is still the stable reference —
titles can change or collide — so print the generic line for every hit:
title in bold, (sessions: <id>) dimmed after it.

Co-Authored-By: Claude Fable 5 <[email protected]>
--session now takes either handle: a raw session id (recognized by its
local transcript, exactly as before) or a session title. Titles match
in both the stored spelling and the VFS safe_name spelling — the one
`stash search` and `stash vfs ls` print — and resolve to the matching
session's id via the overview. No match or an ambiguous title fails
loud, listing the colliding ids.

Co-Authored-By: Claude Fable 5 <[email protected]>
Generalize share's title support into one resolver used by every
session-consuming command: a handle matching a session title (stored or
VFS spelling) resolves to the session's id; anything else passes
through as an id for the server to accept or reject loudly. Ambiguous
titles error, listing the matching ids.

Wired into: share --session, rm/restore/mv session refs, shares add/rm
session objects (these take the row id; the resolver's field arg picks
which id), and agent chat -s / status / watch / stop (chats are
sessions too).

Deliberately not wired into `sessions push`: that's the ingest path —
hooks push events under freshly minted ids at volume, where an
overview fetch per event is waste and a title alias could silently
append to an old conversation instead of starting a new stream.

Co-Authored-By: Claude Fable 5 <[email protected]>
ColemanDuPlessie and others added 2 commits August 10, 2026 12:52
Main independently merged its own 0179 (rewalk_x_bookmarks) plus
0180-0184, so this branch's migration moves to 0185 on top of 0184 —
one head again. The source_service conflict resolves by keeping both
sides: main's modified_after/before pushdown into search_scope_events
and this branch's title-named session hits.

Co-Authored-By: Claude Fable 5 <[email protected]>
The GitHub-side merge of main kept both 0179 migrations (two alembic
heads) and left an unformatted seam in source_service.py — the two CI
failures. This keeps the renumbered 0185 migration and the formatted
source_service resolution.

Co-Authored-By: Claude Fable 5 <[email protected]>
@ColemanDuPlessie

Copy link
Copy Markdown
Collaborator Author

Just updated this, resolving your comment: stash search now returns both title and ID, and titles are sanitized at use, rather than at import.
I also made the CLI commands that take IDs (e.g. stash share) work with IDs or session titles.

I did not, however, allow CLI users to lookup sessions by ID (since currently the system is sessions/[title]/transcript.md): we could add some sort of alias system, but that seems heavy and expensive, so I'm not sure if it's a net win.

@ColemanDuPlessie
ColemanDuPlessie marked this pull request as ready for review August 10, 2026 17:07
…the docs

The CLI resolver landed in b00a452, but four surfaces still named a session
by an id a person never sees.

`stash restore session:"<title>"` could not work: the resolver matches against
the overview, which excludes trashed sessions, so a title fell through as an
id and 404'd. Restore now resolves against the trash — and `stash trash list`
names a session by its title rather than its raw session_id, so the listing
prints the name restore takes. An untitled session still shows its id, via
titles_for_sessions, the one function that answers what a session is called.

The session viewer showed the raw id in three places it already had the title
loaded: the breadcrumb placeholder (now "Session"), the trash-confirm dialog,
and the downloaded transcript's filename (now the title, filename-sanitized).

The chat agent is told to cite a session by title; it is handed titles and no
ids, so citing an id asked for something it does not have.

Ten guidance surfaces documented `stash share <session_id>` as a positional
argument the CLI has never accepted — it is `--session/-s`. Corrected, along
with the www docs entry for `stash sessions transcript`, a command that no
longer exists, replaced by the VFS path that reads a transcript by title.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
One conflict, in cli/tests/test_mount_vfs.py: this branch and main each
appended a FakeClient subclass plus its test at the same point in the file.
The two are independent — the branch's covers shell-hostile characters being
stripped from VFS names, main's covers a table filed inside a skill folder
not taking the mount down — so both are kept, in that order.

Migration numbering needed no change: main tops out at 0184 and this
branch's 0185_session_titles_into_sessions still follows it.
… that resolves

Two gaps meant an agent still had to handle session ids.

The name `ls /sessions` shows for a duplicated title — `<title>--<id8>` — did
not resolve. `_match_session_title` compared against the stored title and its
safe_name spelling only, so the suffixed directory name fell through as an id
and 404'd, while the bare title errored as ambiguous. Both routes dead-ended
on the one case the suffix exists to solve. The VFS's naming is now public as
`stashvfs.session_dir_names`, the filesystem and the resolver both spell names
with it, and resolution accepts that spelling. An ambiguous title now answers
with the suffixed names instead of raw ids, so the error names its own way out.

Resolution was CLI-only, so the MCP tools — the surface an agent is most
likely to be on — took ids and nothing else. It moves to
session_ref_service behind GET /me/sessions/resolve, and the CLI, the MCP
tools, and the sources API all ask the same matcher. stash_session_transcript,
stash_delete_session, stash_assign_session, stash_restore/purge(kind=session),
and read_source(source=sessions) now take a title; source_document resolves
too, so HTTP callers get it without going through the VFS shell.

The pass-through contract is unchanged and now lives in one place: a handle
naming no title is already an id, so the endpoint echoes it back with
matched=false and every caller stays on one codepath. Ambiguity is still
refused rather than guessed — the callers are delete and publish.

Verified end to end against a live backend: transcript-by-title, by the VFS
spelling, read_source by title, a duplicated title refused with both suffixed
names, each of those resolving, and a raw id still passing through.
@ColemanDuPlessie

Copy link
Copy Markdown
Collaborator Author

Updated again: ID is now still mentioned parenthetically in search results, but is entirely redundant (in the CLI; it's still used to generate unique URLs). Everything that models could do in the CLI with a pre-existing transcript's ID can now also be done with the session title.

@henry-dowling

Copy link
Copy Markdown
Collaborator

Closing the loop on this: Confirming that I think we can drop session_id from CLI search results; agents don't really need it for anything

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants