feat: auto-generate session titles via the managed chat_title tool - #2351
Open
7Sageer wants to merge 51 commits into
Open
feat: auto-generate session titles via the managed chat_title tool#23517Sageer wants to merge 51 commits into
7Sageer wants to merge 51 commits into
Conversation
With the auto-title experimental flag on and a managed OAuth login, the
session title is generated from the first prompt, replacing the
truncated-prompt easy title. A custom title set by the user is never
overwritten, and generation failures degrade silently to the easy title.
- oauth: fetchChatTitle for the platform /tools chat_title method
- agent-core (v1): fire-and-forget generation on the first prompt
- agent-core-v2: sessionTitle domain watching the easy-title event
- kap-server: POST /sessions/{id}/title/generate for manual regeneration
🦋 Changeset detectedLatest commit: bbcbc09 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Drop the automatic wiring on both engines: the v1 (TUI) first-prompt trigger and the v2 easy-title event watcher. SessionTitleService's generateTitle() stays as the single on-demand entry point behind the auto-title flag, backing the kap-server title/generate route. The changeset goes away too: with no shipped consumer, the remaining surface is not user-perceivable.
commit: |
Record up to three sanitized natural-language prompts in session metadata (skill / plugin activations excluded) and compose the chat_title input as order-labeled lines truncated to a 1000-char budget, falling back to lastPrompt for sessions without recorded prompts.
# Conflicts: # packages/agent-core-v2/test/session/sessionMetadata/sessionMetadata.test.ts
- persist titleSource (prompt/generated/custom); skip auto-generation over an already-generated title unless forced, and never over a custom one - plumb the force option from the core through klient and node-sdk to the REST title/generate endpoint - drop the title write-back when the session scope was superseded mid-flight, and retry once with a force-refreshed token on a 401 - stop closing sessions a concurrent public resume has handed out in the temporary resume paths (generateSessionTitle, renameSession) - accept session.meta.updated patches without lastPrompt in klient event validation, and emit exactly one metadata event per applied title - remove the retired prompts field heal and drop the changeset (the behavior is only perceivable on the experimental v2 engine)
# Conflicts: # packages/agent-core-v2/docs/state-manifest.d.ts # packages/agent-core-v2/test/workspace/workspaceHandler/workspaceHandler.test.ts # packages/node-sdk/src/sdk-rpc-client-v2.ts
…dler The session title service must not depend on the Workspace-tier handler registry. The handler now seeds each session scope with an abort signal, fires it synchronously when a close begins, and the title service carries the signal on its request, drops the write-back once aborted, and drains an in-flight generation through the onWillCloseSession hook.
A v1 rename spreads the original state.json document, so an explicit isCustomTitle: true can travel with a stale titleKind. The explicit marker now wins on load, and every persist double-writes the derived isCustomTitle so released v1 builds keep recognizing the custom title.
The temporary resume/rename/close paths and the public lifecycle operations now share a per-session queue, so a public resume can never receive a handle whose cleanup close is already in flight. Session summaries carry the canonical title state, letting the TUI skip title generation for sessions whose title was already generated or customized instead of re-asking after every turn.
A close/archive is now tracked in a closing registry from its first synchronous step until disposal: get/list hide the closing session and resume waits the close out instead of returning the doomed handle, and fork waits out an in-flight source close. The title service tracks the whole generateTitle call as the unit the close hook drains, and the generated-title write re-checks the lifetime signal inside the serialized metadata update so an abort landing while the update is queued still vetoes the write-back.
readSummary and the read-model mirror carry titleKind, so listSessions reports the same canonical title state as a resumed session's summary.
forkSession and explicit-id createSession join the per-session queue, and the harness resume fast path skips a session whose close is in flight instead of returning the closing facade (which then failed every call with session.closed); its late onClose no longer evicts the fresh session either. The harness rename event now carries isCustomTitle so the TUI stops asking for a generated title after a local rename.
The closing registry now records the operation kind: an archive arriving during a plain close waits it out and lands the archived flag on the persisted document instead of riding the close to success, and a failing close hook no longer strands a half-closed session — the teardown always completes while the hook error still reaches the caller. create and fork reserve their target id synchronously with the existence check, so a concurrent create/fork of the same id loses up front and can never tear down the winner's scope or directory.
… writes atomically Plain generateTitle calls still coalesce onto one shared in-flight generation, but a forced regeneration always runs on its own so it is neither swallowed by a plain call's early exit nor shares its result; the close hook drains every active generation. The allowWhen veto now runs inside applyUpdate with no await between the check and the mutation, so an abort cannot slip into the gap.
The klient session summary schema no longer strips titleKind, and the index readSummary honors a legacy isCustomTitle marker over a stale titleKind, so listSessions reports the same canonical title state as a resumed session.
… state end to end Concurrent public resumeSession calls now share one in-flight resume and one facade instead of building parallel facades over the same engine handle (a close on either would strand the other). forkSession takes the source and target queues in sorted order, so fork(A->X) is atomic against create(X) and fork(B->X) without an ABBA deadlock. The emitMetaUpdated patch type drops the redundant undefined union, and the SDK tests now cover facade coalescing and the title state across list and resume.
…on the read-model cache readSummary now derives the title state with the same priority chain as the metadata document's canonical normalization (explicit custom marker, valid titleKind, legacy false marker, customTitle, plain title), so list and resume agree on legacy documents too. Read-model cache entries carry a summary version stamp and older-stamped entries are treated as cold misses, so an upgraded reader never serves a stale-shaped summary.
A forced regeneration could be followed on disk by an earlier plain call's slower backend response. Each generation now carries a monotonically increasing sequence (assigned only once a request actually proceeds to generation), and the serialized metadata write is vetoed unless the writer is still the newest request.
An archive requested during a plain close is applied through the live metadata during the teardown (or lands on the persisted document when it arrives too late or the close fails), publishes the archived event, and works on cold sessions too. A resume waiting on a failed close retries instead of propagating the hook error, the teardown completes even when the agent drain fails, and the create/fork rollback only ever removes its own handle — a loser of the reservation race can no longer tear down the winner's live scope.
Concurrent resumes only share a facade when their inputs match — a caller passing different dirs, replay, profile, or kaos options gets its own resume instead of having its options silently dropped.
…ration Auto title is best-effort: a generation racing session close no longer cancels its fetch or guards its write-back, so the per-session sessionLifetime AbortSignal seed, the onWillCloseSession drain, and the close-time invalidation go away. The newest-request-wins write-back predicate stays.
Signed-off-by: 7Sageer <sag77r@hotmail.com>
Nothing calls force: with it gone, plain calls always coalesce onto the shared in-flight generation, so the generation sequence and the caller-supplied allowWhen veto lose their only purpose and go with it. The title/generate REST route takes no body anymore.
…sion index The listed-session titleKind had no consumer: the TUI's title-generation gate seeds from the resumed summary, which reads the live metadata document, and the kap-server REST wire never carried the field. Removing the projection also retires the read-model summary version stamp (the remaining shape is fully field-checkable) and the duplicate title-kind derivation that had to stay in lockstep with sessionMetadata. The klient list contract and the node-sdk list mapper drop the field with it; the resumed/live summary still reports the canonical title state.
…essageLegacy mergeContextTranscriptWithLive had a single caller; move the logic into messageLegacyService as the private mergeLiveTail and drop the export.
# Conflicts: # packages/agent-core-v2/docs/state-manifest.d.ts # packages/agent-core-v2/scripts/check-domain-layers.mjs # packages/agent-core-v2/src/session/sessionMetadata/sessionMetadataService.ts # packages/agent-core-v2/src/workspace/sessionLifecycle/sessionLifecycleService.ts # packages/node-sdk/src/sdk-rpc-client-v2.ts
…ifecycle Auto title no longer consumes close-awareness, so the machinery goes back to the simple forms: close/archive run straight through, resume no longer waits out an in-flight close, create/fork drop their target reservation, and a cold archive is a no-op again. Reverts the behavior of e7c397a and cd1cea0 on top of the sessionLifecycle rename.
Signed-off-by: 7Sageer <sag77r@hotmail.com>
…title test The main merge reduced #/kosong/model/hostRequestHeaders to the pure port contract; define the test's headers as a plain value matching it and tidy the SDK test import grouping.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related Issue
No linked issue — the problem is explained below.
Problem
Session titles are initially derived by truncating the first user prompt to 200 characters. Long or pasted prompts produce unreadable sidebar titles, while the managed platform already exposes a
chat_titletool that can generate a concise title from conversation content. The server stack had no reusable on-demand path for calling that tool and persisting the result.What changed
Adds automatic session title generation for agent-core-v2 sessions authenticated through the managed Kimi Code OAuth provider. As soon as the engine accepts a prompt, the TUI asks it to generate a title from the conversation's first prompts, retrying after each main-agent turn ends until one is applied; a title is generated at most once per session unless explicitly regenerated. This takes effect only on the v2 engine: the default v1 CLI keeps prompt-derived titles, and
kimi webdoes not trigger generation yet.Generation requires a managed OAuth login and otherwise leaves the current title untouched. For logged-in users, up to the first three natural-language user prompts (each prefixed with a plain
user:role marker, capped at 1000 characters combined) are sent to the managed/toolschat_titleendpoint — this applies to sessions on any model provider, consistent with the other managed tools (web search, fetch-url). Sessions without a managed login are unaffected.fetchChatTitlefor the managed platform/toolschat_titlemethod, including authoritative protocol headers, an 8-second timeout, response validation, and structured failures.ISessionTitleServiceand Agent-scoped prompt source. The service coalesces concurrent requests, caps generated titles at 200 characters, retries once with a force-refreshed token on a 401, and drops the write-back when the session scope was closed or superseded mid-flight. Persisted metadata records the title source (prompt/generated/custom): a custom title is never overwritten, and an already-generated title is not regenerated unless forced.POST /api/v1/sessions/{session_id}/title/generateaccepts an optional{ "force": true }body to regenerate an already-generated title. Unknown sessions return40401; unavailable generation returns40922.forceoption through the public facade; fixsession.meta.updatedpatches withoutlastPromptbeing dropped by klient event validation; emit exactly one metadata event per applied title; the temporary resume→act→close paths (generateSessionTitle,renameSession) no longer close a session that a concurrent public resume has handed out.Checklist
gen-changesetsskill;.changeset/auto-session-title.mdannounces the automatic title as aminorCLI entry.gen-docsskill; no user documentation update is needed until a shipped surface exposes this behavior.