Skip to content

fix(runtime): restore automatic Session titles and give the Host the naming effect - #4020

Merged
Astro-Han merged 11 commits into
apache:mainfrom
Astro-Han:fix/session-title-regression
Aug 27, 2026
Merged

fix(runtime): restore automatic Session titles and give the Host the naming effect#4020
Astro-Han merged 11 commits into
apache:mainfrom
Astro-Han:fix/session-title-regression

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

New conversations stopped naming themselves. The auto-title gate required !header.connectionLocked, and PR #3721 made that condition unreachable on the Message path: an admitted root Message is materialized into the transcript before the Run starts, and that append freezes the model route. By the time the Runtime asked "is this Session still unlocked?", the answer was always no, so every Session kept the name New Chat. Dropping that term from the gate restores naming; the "still unnamed" rule was always the real guard.

The gate was only unreachable because the effect had two owners. SessionManager decided when to name, while the Host owned the model call, the residency and the drain — so a timing change inside Host admission could silently disable a Host effect with nothing in the Runtime able to see it. Naming now lives beside recap in the Host Session-effect coordinator, triggered from the one root path that carries a user Message, so a compaction or a continuation still opens a Run without naming anything. SessionManagerDeps loses generateSessionTitle, onSessionTitleChanged and setGeneratedTitleIfAbsent; the Runtime no longer holds any part of a Host-owned effect.

Two cleanups this made visible, rather than a follow-up branch that never opens:

  • Desktop composer sends. sessions:send still ran its own admission ladder — call turn.start, read session_busy off the failure, resubmit as steering, with a Skill carve-out and /skill: content sniffing to keep that ladder honest. PR refactor: make Runtime Host the sole Message admission authority #3803 made turn.message.submit the admission authority, and the composer is not a surface that reserves a Turn. It now submits once under a stable Message identity and maps the Host's disposition; the whole ladder goes.
  • connectionLocked writers. Four writers, three authorities. AgentRun's write at Run start was redundant on both root paths — the Message is already materialized — and the only thing it really carried was subagent Sessions, whose route is chosen by the spawn that created them and is never re-targeted. That fact belongs at creation, so a subagent Session is now born frozen, with a metadata migration backfilling existing lineages.

Verification

  • New packages/runtime-host/src/__tests__/execution-host-session-title.test.ts drives a real forked Host and covers both root paths end to end: a first turn.message.submit and a first turn.start each name their default-named Session, and each leaves the Session's route frozen. It fails on main with 'New Chat' !== 'draft the release notes' on the submit case.
  • @maka/runtime 2911, @maka/runtime-host 1279, @maka/storage 975 and the Desktop main suite 1578 pass; desktop typecheck, biome format and biome lint clean.
  • Not run: the full Electron end-to-end suite.

Review focus

Behavior given up on purpose: a busy Desktop send no longer degrades a Skill invocation to steering locally — the Host's session_busy propagates instead. The queued path resolves a textual /skill: token without reporting partial resolution back to the composer; that gap is tracked in #4026. Retry semantics tighten as a side effect, since every send now carries a caller-owned messageId, so an interrupted dispatch is safe to repeat and only a second lost answer resolves as outcome_unknown.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Claude Code — diagnosis, implementation and tests, reviewed by the author. Every commit carries Generated-by: Claude Code.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

…handoff

Durable Message admission materializes the admitted user Message before the
Run starts, and materializing a user Message locks the Session's connection.
The automatic-title gate read that lock as "this Session already ran a Turn",
so every Turn started through `turn.message.submit` — which is now every
Desktop and CLI send — was rejected before the title effect could run, and
not even the offline fallback name landed.

The Session name is the only authority for "this Session is still unnamed",
so the gate keeps `titleIsManual` and the default-name check and drops the
connection lock. `setGeneratedTitleIfAbsent` still re-checks both under its
own write, so a racing manual rename still wins.

Covers both root paths end to end against a real Host: a first
`turn.message.submit` and a first `turn.start` each name their Session.

Generated-by: Claude Code
The title effect was split across two owners: SessionManager decided when a
Session should be named, while the Host owned the model call, the residency,
and the drain. That split is what broke naming — the gate read a header
snapshot whose timing the Host's Message admission had quietly changed, and
nothing in the Runtime could see that.

Naming now lives beside recap in the Host Session-effect coordinator, which
reads the header, applies the "still unnamed" rule, falls back to the
Message's first line when the title model is unreachable, and writes through
`setGeneratedTitleIfAbsent`. The root Turn coordinator triggers it from the
one path that carries a user Message, so a compaction or a continuation opens
a Run without naming anything — as before.

`SessionManagerDeps` loses `generateSessionTitle` and `onSessionTitleChanged`,
and its Session store port loses `setGeneratedTitleIfAbsent`; the Runtime no
longer holds any part of a Host-owned effect.

Also removes `generateSessionTitle` from `@maka/runtime/session-title`: it was
a second, unused implementation of the same model call — no production
consumer, no telemetry, no pricing — that only its own tests kept alive. The
prompt, the cleaner, and the fallback stay, and the cleaner's coverage now
targets the cleaner directly.

Generated-by: Claude Code
`sessions:send` kept its own admission ladder: it called `turn.start`
first, read `session_busy` off the failure, and only then resubmitted the
same text as steering — with Skill sends carved out so they would not
silently degrade. That ladder predates PR apache#3803, which made
`turn.message.submit` answer `steering | followup | turn_started | blocked`
and left `turn.start` for the surfaces that genuinely reserve a Turn.

The Desktop composer is not one of them, so it now submits once under a
stable Message identity and maps the Host's disposition. The busy race, the
Skill carve-out, and the content sniffing for `/skill:` tokens all
disappear: the Host already owns those decisions.

Retry semantics improve as a side effect. Every send now carries a caller-
owned `messageId`, so an interrupted dispatch is safe to repeat and only a
second lost answer resolves as `outcome_unknown`.

Generated-by: Claude Code
`connectionLocked` was written from four places. Three are authorities: the
Session store freezes a route when the first user Message is appended,
configuration update re-freezes on retarget, and Revision inheritance carries
the frozen flag. The fourth was AgentRun, which set the flag again at Run
start.

On both root paths that write is redundant — the Message is materialized
before the Run opens, so the header is already locked. The one thing it did
carry was subagent Sessions, which never see a user Message and so were being
frozen as a side effect of their first Run.

That fact belongs at creation: a subagent Session's route is chosen by the
spawn that created it and is never re-targeted, so it is now born locked.
AgentRun stops writing headers it does not own.

Generated-by: Claude Code
@github-actions github-actions Bot added the effort/L Under 1000 readable lines label Aug 27, 2026
Adversarial review of the submit-only composer path found three seams the
rewrite left behind.

`outcome_unknown` is now the main failure shape of a send — the Host declining
to prove what happened, not refusing the Message. WorkHub read every `ok:false`
as a refusal and released the reserved root, so a Message that may already be
running stopped being reconcilable. It now maps to the `unknown` admission the
reconciliation machinery was built for. Its test used a `reason` the contract
does not have, which is why the mapping looked covered.

The `blocked` disposition returned a shape the bridge contract does not
declare: no `reason`, so the renderer reported `send failed: undefined`. It now
answers `skill_invocation_failed`, like the neighbouring `submitMessage`
handler, and has a test.

The busy-Skill test asserted a contract the Host does not offer. Only explicit
`skillIds` and an orchestration override are exact-Turn intent; a `/skill:`
token in the text is expanded on the queued path too, so a busy Session steers
it rather than refusing it. The two shapes are now covered separately, and the
UDS facade test drives the real submit operation instead of `turn.start`.

Generated-by: Claude Code
Adversarial review of the naming effect found the safety argument resting on
`setGeneratedTitleIfAbsent`, which was a read-then-write: a manual rename
landing between the check and the write was overwritten, leaving the Session
with `titleIsManual: true` and a generated name. Restoring naming widened that
window from a Session's first Turn to every Turn it spends unnamed, so the
write now happens at the revision the check read and answers a lost race with
`null`.

Naming also stops racing itself: a queued Message can open its Turn while the
first title call is still out, and the second call could only lose the write,
so one attempt per Session is in flight at a time.

The two effects now report failure the same way. A rename that wins is an
answer and stays silent; a Session store that cannot answer at all requests a
drain, as recap already did.

Covers the seam a mutation proved untested: no test distinguished the model's
title from the fallback, so dropping the generated value entirely kept every
naming test green.

Generated-by: Claude Code
Adversarial review disproved the premise the previous commit was written on:
a subagent Session does see a user Message. Its spawn opens the first Turn
through `sendMessage`, and the store locks the route on that append like any
other Session. Freezing at creation is still right — the route is chosen by
the spawn and never re-targeted, so it need not wait for the prompt to land —
but it closed a gap of milliseconds, not the gap the commit claimed.

The real gap is on disk. A subagent abandoned before its first Message keeps
`connectionLocked: false` forever now that AgentRun no longer writes it, and
nothing else will ever lock it: migration 22 only reached Sessions that have a
user Message. Migration 33 locks them by lineage.

The new authority also had no production coverage — the only assertions ran
through a test double changed in the same commit — so the Session store now
proves both halves directly: an ordinary Session is born unlocked, a subagent
locked.

Removes the last two `connectionLocked: true` overrides, in the in-memory
child headers the Runtime kernel builds for same-Session child Turns. They
existed only to make AgentRun skip the write that is now gone; their
`updateHeader` hook never persisted anything. AgentRun's header is `readonly`,
which is what "stops writing headers it does not own" should look like.

Generated-by: Claude Code
@Astro-Han

Copy link
Copy Markdown
Contributor Author

Adversarial review pass

Three subagents reviewed this branch independently (naming effect, Desktop send path, connectionLocked), each required to try to refute its own findings. Seven survived; all are fixed in 6eb8335b0, f1922387f and 2f80611c7.

Desktop send path

  • outcome_unknown is now the main failure shape of a send — the Host declining to prove what happened, not refusing the Message — but WorkHub read every ok:false as a refusal and released the reserved root, so a Message that may already be running stopped being reconcilable. Its test used a reason the contract does not have, which is why the mapping looked covered.
  • The blocked disposition returned a shape the bridge contract does not declare (no reason), so the renderer reported send failed: undefined.
  • The busy-Skill test asserted a contract the Host does not offer. Only explicit skillIds and an orchestration override are exact-Turn intent; a /skill: token in the text is expanded on the queued path too. That is this PR's one user-visible behavior change, and it was hidden behind an unconditional stub — now covered as two separate cases.

Naming effect

  • setGeneratedTitleIfAbsent was a read-then-write, so a manual rename landing between check and write was overwritten, leaving titleIsManual: true on a generated name. Restoring naming widened that window from a Session's first Turn to every Turn it spends unnamed. It is now a conditional write at the revision the check read.
  • A queued Message could open its Turn while the first title call was still out, spending a second auxiliary model call that could only lose the write.
  • A mutation proved a coverage gap: discarding the model's title entirely kept every naming test green, because no test distinguished it from the fallback.

connectionLocked

  • The premise of 7b2557372's message is wrong, and 2f80611c7 says so: a subagent Session does see a user Message — its spawn opens the first Turn through sendMessage. Freezing at creation is still right, but it closes milliseconds, not the gap claimed. The real gap is on disk: a subagent abandoned before its first Message can no longer be locked by anything, since migration 22 only reached Sessions that have a user Message. Migration 33 locks them by lineage.
  • The new authority had no production coverage — its only assertions ran through a test double changed in the same commit.

Two reported findings did not survive verification and were left alone: a claimed stop no-op under Host-minted Turn ids (a red test for it passed unchanged — reconcileUncertainAdmissions already resolves the candidate before a correction stops it), and a claimed messageId collision risk (both call sites mint a fresh identity per send).

Suites: @maka/runtime 2916, @maka/runtime-host 1274, @maka/storage 973, @maka/desktop main 1577 — all passing. The one computer-use-host failure reproduces on a clean tree and is unrelated.

With naming rehomed to the Host, `@maka/runtime/session-title` had no consumer
left inside the Runtime: the prompt, the cleaner, the timeout, the source
extractor and the fallback are read only by the Host's Session-effect
coordinator and its model authority. The module was the last residue of the
Runtime's old half of the effect.

It now sits beside them in `@maka/runtime-host`, and `@maka/runtime` drops the
`./session-title` export subpath. Nothing publishes these packages, so a
subpath with no importer in the repository has no other demand to serve.

Generated-by: Claude Code
…ranscript

Second review round found a deadlock this branch made reachable. The side-chat
panel treats `outcome_unknown` as a pending admission and waits for a
`message_admission` event to name the Turn — but the Host projects that event
only for steering Messages, which carry a `steeringEventId`. Until now the only
way to reach `outcome_unknown` there was the busy fallback, which always
steered; a first send on an idle fork went through `turn.start` and answered
with a Turn id directly. Submitting instead means a lost answer on an idle fork
leaves the panel processing forever, with send and stop both refusing to act.

The panel now reconciles the way WorkHub already does: a root Message is
materialized before its Run starts, so the durable transcript names the Turn it
opened under the identity the panel sent. It reconciles once when the send
answers unproven, and again whenever the fork produces a Turn event it cannot
attribute — which is exactly the evidence that something of its own may be
running.

Also settles two smaller findings from the same round. The Session-effect
coordinator no longer drains the Host when a generated title fails to persist:
recap drains because it owes a caller an answer it cannot give, while naming
owes nobody, and losing a name is not worth retiring a Host that is running
Turns. And `setGeneratedTitleIfAbsent` re-reads on a version conflict instead of
reporting one as "already named", so only a real rename ends the attempt.

Generated-by: Claude Code
A mutation proved the gap: reverting `setGeneratedTitleIfAbsent` to an
unconditional write left every suite green, so the rename it is meant to
protect could regress silently. `setGeneratedTitleIfAbsent` appeared in no test
in the repository — the coordinator's racing-rename test stubs the store out
and asserts what the coordinator does with `null`.

The Session store now proves all three answers directly: a generated title
fills an absence and is refused once a Session has a name; a rename landing
between the check and the write keeps the user's name and its manual flag; and
a revision that moved for any other reason is re-read rather than mistaken for
one.

Also states in the bridge contract that a successful `sessions.send` answers
with the Turn Runtime Host minted, not the identity the caller reserved.

Generated-by: Claude Code
@Astro-Han

Copy link
Copy Markdown
Contributor Author

Adversarial review, round 2

Three more subagents reviewed the branch: one on the round-1 fixes themselves, one on main...HEAD as a single change, one end-to-end from the renderer to the Host. Verdict on the whole: converged — the third reviewer found no lost behavior and no split authority, and proved the naming trigger by mutation (removing the call in root-turn-coordinator turns both end-to-end tests red on 'New Chat').

Three findings survived verification; all are fixed in 16236c9a9 and 8605fa7b5.

P1 — the side-chat panel could deadlock (newly reachable on this branch). It treats outcome_unknown as a pending admission and waits for a message_admission event to name the Turn, but the Host projects that event only for steering Messages, which carry a steeringEventId. Previously the only route to outcome_unknown there was the busy fallback, which always steered; a first send on an idle fork went through turn.start and answered with a Turn id. Submitting instead means a lost answer on an idle fork leaves the panel processing forever, with send and stop both refusing and the composer already cleared. It now reconciles the way WorkHub does — a root Message is materialized before its Run starts, so the transcript names the Turn it opened under the identity the panel sent.

P2 — naming no longer drains the Host on a failed write. Round 1 had me raise a failed title persist to requestDrain(), which is process-level retirement of every Turn on that Host. Recap drains because it owes a caller an answer it cannot give; naming owes nobody. A busy SQLite or a disk hiccup is not worth retiring a Host that is running work.

P2 — the conditional title write had no test at all. A mutation back to an unconditional write left every suite green. setGeneratedTitleIfAbsent appeared in no test in the repository; the coordinator's racing-rename test stubs the store out. The Session store now proves all three answers, and a version conflict is re-read rather than reported as "already named".

Deliberately not in this PR: on a busy Session, a Skill that partly or wholly fails to resolve is silent, because the steering/followup results of turn.message.submit carry no skillInvocation field. Plumbing it spans the protocol, the message coordinator, the root Turn coordinator and the Desktop — a separate intent, and I'd rather it be reviewed as one.

@YayoiNanoka YayoiNanoka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved — I found no reachable P0/P1 blocker at 8605fa7b50b84bdab14eba7151bc1e95a0c519af.

The naming effect is limited to root turns that carry a user message; compaction and continuation paths do not trigger it. The generated-title write is revision-fenced, so a concurrent manual rename wins. Model-route freezing remains authoritative through the first durable user-message append and subagent creation. The Desktop admission rewrite keeps one caller-owned message identity and preserves unknown outcomes for transcript reconciliation instead of dropping potentially accepted input.

Verification on the PR head and on a clean merge with current main:

  • npm run build:test passed.
  • 452 targeted storage, Runtime, Runtime Host, UDS, Desktop admission, Side Conversation, and WorkHub tests passed.
  • No temporary test was committed or pushed; the original workspace was unchanged.

Hosted checks were still settling when I reviewed; the earlier Windows Gitoxide-helper failure was outside the changed paths and its rerun was pending. This approval records the requested code-risk threshold, not a claim that all hosted checks are complete.

点击展开中文

已批准——在 8605fa7b50b84bdab14eba7151bc1e95a0c519af 上未发现真实可达的 P0/P1 阻塞问题。

命名副作用仅由携带用户消息的根 Turn 触发;压缩与续跑路径不会触发。生成标题的写入受 revision 栅栏保护,因此并发的手动重命名会胜出。模型路由仍通过首条持久化用户消息的追加以及 subagent 创建来冻结。Desktop 的准入重写保持一个由调用方持有的消息身份,并把未知结果留给 transcript 对账,不会丢弃可能已经被 Host 接受的输入。

已在 PR head 和与当前 main 的干净合并结果上验证:

  • npm run build:test 通过。
  • 452 个针对 storage、Runtime、Runtime Host、UDS、Desktop 准入、Side Conversation 与 WorkHub 的测试全部通过。
  • 没有提交或推送临时测试;原工作区未改动。

审查时托管检查仍在收敛;此前的 Windows Gitoxide helper 失败不在本 PR 改动路径内,其重跑仍在等待。本次批准表示已满足要求的代码风险阈值,不代表所有托管检查已经完成。

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving. No blocking issues. I gave the storage migration the most attention, since a schema change that leaves existing installs inconsistent is the failure that users cannot work around.

The schema change is complete

SQLITE_SESSION_METADATA_SCHEMA_VERSION goes 32 to 33 with migration 33 added in the same commit, and the module's own MIGRATIONS.size !== VERSION assertion still holds — I confirmed 33 entries, max key 33, version 33.

The backfill is the part that matters, and it is correctly scoped:

WHERE json_extract(payload_json, '$.connectionLocked') = 0
  AND json_extract(payload_json, '$.subagentParent') IS NOT NULL

That is exactly the set whose invariant changed — existing subagent Sessions that were unlocked at creation and would otherwise never be locked now that AgentRun no longer writes the flag. It is also the same SQL shape as migration 22, down to the json('true') and the MAX(committed_at, ...) clamp, so it follows the established idiom rather than inventing one.

The diagnosis holds up against the diff

The claim is that the auto-title gate became unreachable because it required !header.connectionLocked while an admitted root Message freezes the route before the Run starts. The removed code in session-manager.ts confirms the first half directly — the gate really was !header.connectionLocked && !header.titleIsManual && header.name === DEFAULT_SESSION_NAME && sourceText. Dropping that one term and keeping the name check is the minimal fix.

Other things I verified

  • Naming hangs off the right path. startRootMessageTurn carries the hook, and the resumeSafeBoundaryContinuation branch keeps the bare onRunStarted, so a compaction or continuation opens a Run without naming. That is what the description promises and the ternary shows it plainly.
  • The optional hook is actually wired. nameSessionFromRootMessage is declared ?.-optional on the constructor, which would let a missing wire silently reproduce the original bug. It is passed at execution-composition.ts:1105, and nameSessionIfUnnamed / onSessionNamed are wired alongside it.
  • The one-at-a-time guard is sound. nameSessionFromRootMessage scans #titleAborts and registers the new controller with no await in between, so two calls in the same tick cannot both pass the check.
  • The file move left nothing dangling. ./session-title is removed from packages/runtime's exports in the same commit the file moves out, and no reference to it survives in that package — so no export pointing at a dist file that will not exist.

Two small things, neither blocking

1. Aborting a title call does not actually stop it (P3). shutdown aborts every controller in #titleAborts. In #nameSession, the catch around generateTitle does not distinguish an abort from an unreachable model, so an aborted call falls through to fallbackSessionTitle(sourceText) and still performs the store write. The effect is cosmetic — a Session named from the Message's first line instead of the model's phrasing when the Host shuts down mid-call — and the write is wrapped, so nothing can throw out of it. But it does mean abort downgrades the effect rather than cancelling it, which is not what the surrounding comment ("a store that cannot answer leaves the Session unnamed for the next root Message to retry") leads a reader to expect. An if (abortSignal.aborted) return; before the write, or a sentence saying the fallback is deliberate on abort too, would settle it.

2. setGeneratedTitleIfAbsent throws where its type suggests it returns (P3). After three version conflicts the loop rethrows SessionMetadataVersionConflictError rather than answering null, which makes the trailing return null unreachable. The only caller today wraps it in a catch, so nothing is wrong in this PR. It is the next caller that reads Promise<SessionHeader | null>, treats null as "did not happen", and does not expect a throw.

Note on state

test was still in_progress when I finished, with audit, ubuntu-latest, macos-latest, windows-latest and windows_recovery already green. Nothing in my review depends on it, but the unit suites are what would catch a regression in the naming path, so it is worth confirming terminal green before this moves.

I also see @YayoiNanoka approved this same head, so treat this as a second pass rather than the first.


Posted by an automated review agent operated by @jackwener. This is not an
independent human review and does not satisfy the committer review required by
CONTRIBUTING.md. A human is accountable for this comment — please push back if
anything here is wrong.

简体中文

本条评论由 @jackwener 运行的自动化审查程序发出。它不构成 CONTRIBUTING.md
所要求的独立人类审查,也不能替代人类审查。有人类对本条评论负责,如有错误请直接指出。

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approved at exact head 8605fa7b50b84bdab14eba7151bc1e95a0c519af: I found no reachable P0/P1 blocker.

The change restores automatic Session naming under Runtime Host ownership, revision-fences generated titles so a concurrent manual rename wins, and removes the Desktop turn.start → busy → submit admission ladder without losing message identity or unknown-outcome reconciliation. Route freezing and connectionLocked now each have one durable authority, including subagent lineage migration.

I independently reproduced one non-blocking P2 already disclosed in the author's round-two review note: a busy textual /skill: message can execute its successfully resolved Skills while the Desktop loses partial-resolution feedback. All-failed resolution and explicit exact-Turn Skill intent still fail closed, so this does not cross the requested P0/P1 blocking threshold; I am not duplicating the existing public report as another inline thread.

Verification on this head:

  • 555 focused Storage, Runtime, Runtime Host, Desktop admission, Side Conversation, WorkHub, SessionManager, and AgentRun tests passed.
  • Core, Storage, MCP, Runtime, Runtime Host, Computer Use, Eval, CLI, Desktop main, and Desktop preload builds passed.
  • Biome and git diff --check passed for the changed files.
  • The current-main synthetic merge was clean; its only overlapping bridge-contract edit combined additively.
  • Hosted exact-head checks are terminal green.

Posted by an automated review agent operated by @M4n5ter. This is not an
independent human review and does not satisfy the committer review required by
CONTRIBUTING.md. A human is accountable for this comment — please push back if
anything here is wrong.

简体中文

本条评论由 @M4n5ter 运行的自动化审查程序发出。它不构成 CONTRIBUTING.md
所要求的独立人类审查,也不能替代人类审查。有人类对本条评论负责,如有错误请直接指出。

Two review follow-ups on the Session naming effect, both about a path
answering something other than what its shape promises.

Shutdown aborts the title call, but the catch around the model did not
tell an abort apart from an unreachable model, so a draining Host still
wrote the Message's first line as the name. Abort now retires the
effect: the next root Message names the Session, which is what the
surrounding comment already told a reader to expect.

`setGeneratedTitleIfAbsent` rethrew the version conflict on its last
attempt, so a caller reading `SessionHeader | null` had to also expect a
throw for the one outcome the null already means. Exhausting the
attempts now answers null like any other lost race; a conflict is the
only error it swallows.
@Astro-Han
Astro-Han merged commit bd9e4e0 into apache:main Aug 27, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/L Under 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants