Skip to content

fix: manual rename reverts to folder default after a reload race (TitleLocked lost on reapply)#1570

Open
pablosiber wants to merge 2 commits into
asheshgoplani:mainfrom
pablosiber:fix/pending-title-restores-lock
Open

fix: manual rename reverts to folder default after a reload race (TitleLocked lost on reapply)#1570
pablosiber wants to merge 2 commits into
asheshgoplani:mainfrom
pablosiber:fix/pending-title-restores-lock

Conversation

@pablosiber

@pablosiber pablosiber commented Jul 5, 2026

Copy link
Copy Markdown

Symptom

A session I renamed by hand keeps snapping back to the default <folder>-<hash> name — not once, but intermittently across restarts. In the DB the affected rows have title_locked = 0, while renames that did stick have title_locked = 1.

Root cause

The pending-title reapply path drops the lock.

pendingTitleChanges exists so a rename survives a storage-watcher reload that races the save (isReloading=truesaveInstances() is a no-op → the reload replaces h.instances with stale disk rows). But the map stored only the title string:

  • On rename, session.SetField(FieldTitle, …) correctly sets Title + SetAutoName(false) + TitleLocked = true, and queues the title in pendingTitleChanges.
  • A racing reload swaps in the stale row, where title_locked = 0 (the lock was never persisted).
  • The reapply loop in loadSessionsMsg restores Title and clears auto-name, but never re-sets TitleLocked — so the row is now correctly-named but unlocked.
  • On the next hook/turn boundary, ReconcileTitleFromClaude sees !TitleLocked, reads Claude Code 2.1.19x's auto-derived cwd-folder name (nameSource="derived"), and overwrites the title back to the folder default.

This is adjacent to #1545 (which stopped the derived name from clobbering the auto_name handle) but distinct: here a genuine user rename loses its lock and is reverted. It's a race, which is why it recurs intermittently.

Fix

Carry the lock intent through the reload swap. pendingTitleChanges becomes map[string]pendingTitle{title, locked}. All three writers capture inst.TitleLocked at enqueue time:

Writer Origin Captured lock
handleEditSessionDialogKey (SetField dispatch) user rename true
handleGroupDialogKey (TUI rename) user rename true
attachSessionReconcileTitleFromClaude Claude-name sync false

The reapply restores TitleLocked from the queued value — so a user rename stays locked (survives the #572 sync) while a sync-sourced title stays unlocked (keeps tracking Claude's session name). The unlocked case is why a blanket TitleLocked = true would be wrong.

Tests

  • New TestHomeRenamePendingChangeRestoresTitleLock with both directions (user rename → relocked; sync title → stays unlocked). Verified it fails on main (TitleLocked = false after reapply, want true) and passes with the fix.
  • Existing TestHomeRenamePendingChanges* updated to the struct and still green.
  • go build, go vet, gofmt -l, and the full ./internal/ui/ + ./internal/session/ suites all pass locally (go 1.25.11).

Notes

Found while on v1.9.73; the derived-name interaction is more visible since Claude Code 2.1.19x started auto-deriving folder names. Reproduces on current main (v1.10.9) via the added test.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed pending session title renames to persist correctly across reloads.
    • Improved rename behavior to reliably restore and preserve whether a title is locked or unlocked after refresh.
    • Ensured pending rename updates are not dropped during session refresh/save and are correctly reconciled with sync-derived title changes.
  • Tests
    • Updated and expanded coverage for pending rename persistence and title-lock restoration, including reload race scenarios.

…ad race

A manual session rename could silently revert to the cwd-folder default on
the next turn. When a rename's save was skipped (isReloading=true), the title
was queued in pendingTitleChanges and re-applied after the storage-watcher
reload swap — but the queue stored only the title string, dropping its
TitleLocked intent. The reapplied title came back unlocked, so the next asheshgoplani#572
Claude-name sync (ReconcileTitleFromClaude) overwrote it with Claude Code
2.1.19x's auto-derived folder name.

Carry the lock state alongside the title (new pendingTitle struct). All three
writers capture inst.TitleLocked at enqueue time — true for a user rename via
SetField, false for a sync-sourced title — and the reapply restores it, so a
user rename stays locked while a synced title keeps tracking Claude.

Pinned by TestHomeRenamePendingChangeRestoresTitleLock (both directions).
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0d5590e2-7a7c-44ff-ba4f-51d95db1d60b

📥 Commits

Reviewing files that changed from the base of the PR and between d5f19e5 and e5875c2.

📒 Files selected for processing (2)
  • internal/ui/home.go
  • internal/ui/home_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/ui/home_test.go
  • internal/ui/home.go

📝 Walkthrough

Walkthrough

This PR introduces a pendingTitle struct that pairs a session title with its TitleLocked state, replacing the plain-string pendingTitleChanges map. Reload re-application, save clearing, enqueue sites, and tests are updated to preserve the lock state.

Changes

Pending title lock persistence

Layer / File(s) Summary
pendingTitle struct and map type definition
internal/ui/home.go
Adds pendingTitle{title, locked} and changes pendingTitleChanges plus its initialization to map[string]pendingTitle.
Reload re-application and save clearing
internal/ui/home.go
On reload, reapplies title and restores TitleLocked from queued entries, forces SetAutoName(false), and clears the map; after save, clears the typed map.
Enqueue sites for pending title changes
internal/ui/home.go
Edit-session, group rename, and Claude reconciliation now enqueue pendingTitle{title, locked} values.
Test updates and new lock regression test
internal/ui/home_test.go, internal/ui/tui_eval_seam_b_test.go
Updates existing tests and the seam constructor to use pendingTitle; adds a reload-race regression test for restoring TitleLocked.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related PRs

Suggested reviewers: asheshgoplani

🚥 Pre-merge checks | ✅ 7
✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title uses Conventional Commits format and accurately describes the rename reload race fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Remote_parity ✅ Passed The new regression test explicitly documents that RemoteSession can't reach the local pending-title reapply path, so the exclusion is intentional.
Test_coverage_per_surface ✅ Passed PASS: the change is TUI-only; new home tests cover reload reapply, auto-name clearing, and relock/unlock, and web UI explicitly lacks a rename API, so other surfaces don't apply.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/ui/home_test.go (1)

729-805: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider documenting RemoteSession non-applicability.

This new test exercises the pending-title reload/lock-reapply path but neither covers a RemoteSession case nor documents why one is unnecessary. Since pendingTitleChanges reapply is keyed by session ID and resolved via getInstanceByID (local *session.Instance only), RemoteSession (which has no local Instance) is likely structurally inapplicable here — analogous to the precedent for fork-gating tests. Adding a short t.Skip (or comment) documenting this would satisfy the project convention for TUI-behavior test changes.

📝 Suggested documentation addition
 func TestHomeRenamePendingChangeRestoresTitleLock(t *testing.T) {
+	// Note: RemoteSession has no local *session.Instance and is never keyed
+	// into pendingTitleChanges/getInstanceByID, so it cannot reach this
+	// reload-reapply path. No RemoteSession case is needed here.
 	t.Run("user rename is relocked", func(t *testing.T) {

Based on coding guidelines, "When a PR changes TUI behavior in internal/ui/ or cmd/agent-deck/, verify it also handles RemoteSession or include an explicit t.Skip documenting why".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/ui/home_test.go` around lines 729 - 805, The new
TestHomeRenamePendingChangeRestoresTitleLock in home_test.go covers the
pending-title reapply path for local instances but does not address
RemoteSession applicability. Add a short t.Skip or an explanatory comment in
this test to document that pendingTitleChanges is resolved through
getInstanceByID on local *session.Instance objects, so RemoteSession is
structurally not applicable here. Keep the note near
TestHomeRenamePendingChangeRestoresTitleLock so future TUI behavior changes
understand why no RemoteSession coverage is needed.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/ui/home.go`:
- Around line 9711-9722: The local rename path in
h.getInstanceByID/session.SetField mutates inst.Title and inst.TitleLocked
without the same h.instancesMu protection used elsewhere, so update the rename
flow in home.go to hold h.instancesMu around this mutation or delegate to the
existing locked helper. Keep the lock consistent with the other instance
mutators and make sure the pendingTitleChanges update still happens after the
protected rename.

---

Nitpick comments:
In `@internal/ui/home_test.go`:
- Around line 729-805: The new TestHomeRenamePendingChangeRestoresTitleLock in
home_test.go covers the pending-title reapply path for local instances but does
not address RemoteSession applicability. Add a short t.Skip or an explanatory
comment in this test to document that pendingTitleChanges is resolved through
getInstanceByID on local *session.Instance objects, so RemoteSession is
structurally not applicable here. Keep the note near
TestHomeRenamePendingChangeRestoresTitleLock so future TUI behavior changes
understand why no RemoteSession coverage is needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e7ccfa9-cf25-4584-a781-ab9b328595a9

📥 Commits

Reviewing files that changed from the base of the PR and between f70f19e and d5f19e5.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !**/*.md, !CHANGELOG.md
📒 Files selected for processing (3)
  • internal/ui/home.go
  • internal/ui/home_test.go
  • internal/ui/tui_eval_seam_b_test.go

Comment thread internal/ui/home.go
…ession N/A

Addresses CodeRabbit review on asheshgoplani#1570:
- Hold h.instancesMu around the group-dialog SetField mutation (mirrors the
  edit-dialog rename path); run the tmux-heavy postCommit after unlocking so a
  slow subprocess can't stall the status worker / reconciler.
- Document why the new lock-reapply test needs no RemoteSession case
  (pendingTitleChanges resolves via getInstanceByID, local instances only).
@pablosiber

Copy link
Copy Markdown
Author

Thanks @coderabbitai — both points addressed in e5875c2:

  • instancesMu on the group-dialog rename: wrapped the SetField mutation in h.instancesMu to match the edit-dialog rename path, and moved the (tmux-heavy) postCommit to run after unlocking so a slow subprocess can't stall the status worker / reconciler.
  • RemoteSession non-applicability: added a comment on TestHomeRenamePendingChangeRestoresTitleLock documenting that pendingTitleChanges is resolved via getInstanceByID (local *session.Instance only) and remote renames go through the separate SSH-runner branch, so they never reach this reload-reapply path.

gofmt, go vet, and the full ./internal/ui/ + ./internal/session/ suites pass locally.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cd /repo 2>/dev/null || true
git log --oneline -10
git show e5875c2 --stat 2>/dev/null || echo "commit not found in shallow clone"

Length of output: 1866


🏁 Script executed:

#!/bin/bash
cd /repo 2>/dev/null || true
git show e5875c2 -- internal/ui/home.go

Length of output: 2398


The diff confirms both points were addressed correctly:

  • The SetField mutation is now wrapped under h.instancesMu.Lock()/Unlock(), matching the edit-dialog rename path, and the heavier postCommit callback runs after the unlock so it won't block the status worker or reconciler.
  • The TestHomeRenamePendingChangeRestoresTitleLock test now has a comment explaining why RemoteSession isn't applicable, since pendingTitleChanges is resolved via getInstanceByID against local instances only.

Looks good — thanks for the quick turnaround, @pablosiber!

[resolve]

(^▽^)

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