Skip to content

feat: auto-generate session titles via the managed chat_title tool - #2351

Open
7Sageer wants to merge 51 commits into
mainfrom
auto-title
Open

feat: auto-generate session titles via the managed chat_title tool#2351
7Sageer wants to merge 51 commits into
mainfrom
auto-title

Conversation

@7Sageer

@7Sageer 7Sageer commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

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_title tool 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 web does 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 /tools chat_title endpoint — 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.

  • oauth: add fetchChatTitle for the managed platform /tools chat_title method, including authoritative protocol headers, an 8-second timeout, response validation, and structured failures.
  • agent-core-v2: add the Session-scoped ISessionTitleService and 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.
  • kap-server: POST /api/v1/sessions/{session_id}/title/generate accepts an optional { "force": true } body to regenerate an already-generated title. Unknown sessions return 40401; unavailable generation returns 40922.
  • node-sdk / klient: plumb the force option through the public facade; fix session.meta.updated patches without lastPrompt being 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.
  • TUI: request title generation as soon as a prompt is accepted, retrying after each main-agent turn ends until one succeeds and disabling on hard failures.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added unit and integration coverage for the managed request contract, prompt selection, title-source persistence and races, 401 refresh, the stale-scope guard, event validation, and the public REST path.
  • Ran gen-changesets skill; .changeset/auto-session-title.md announces the automatic title as a minor CLI entry.
  • Ran gen-docs skill; no user documentation update is needed until a shipped surface exposes this behavior.

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-bot

changeset-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bbcbc09

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Minor

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

7Sageer added 6 commits July 29, 2026 16:15
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.
@pkg-pr-new

pkg-pr-new Bot commented Jul 29, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@bbcbc09
npx https://pkg.pr.new/@moonshot-ai/kimi-code@bbcbc09

commit: bbcbc09

7Sageer added 21 commits July 30, 2026 12:10
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.
7Sageer and others added 23 commits July 31, 2026 13:34
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.
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.

1 participant