Skip to content

Fixed session name not saved to the stopped session in the Save & Switch dialog - #206

Merged
oleg68 merged 111 commits into
nbplugins:mainfrom
oleg68:bugfix/close-dialog-session-name-not-saved
Nov 13, 2026
Merged

oleg68 merged 111 commits into
nbplugins:mainfrom
oleg68:bugfix/close-dialog-session-name-not-saved

Conversation

@oleg68

@oleg68 oleg68 commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Problem

In the Save & Switch dialog, a name entered for the session being closed could be written into the wrong session file, or lost entirely — most visibly when choosing Restart Advanced... with a named connection profile (e.g. Work, which has an isolated config dir) and with several sessions/tabs in the same project.

Three combined causes:

  1. stopAndRename() passed configDir = null, so renames always went to ~/.claude, ignoring the profile's isolated store (and corrupting an unrelated session there).
  2. The target session was guessed via findMostRecent() — the newest .jsonl by mtime, which can be another tab's session; it also returns null for an unparseable newest file and silently skipped the rename.
  3. The name field was prefilled with SavedSession.displayName(), which falls back to the session ID — a brand-new session showed a UUID, and accepting it stored a UUID as the title.

Fix

The controller is now the single source of truth for the running session's identity, captured at launch:

  • effective CLAUDE_CONFIG_DIR (profile extra env vars included, else ~/.claude);
  • session ID: RESUME_SPECIFIC → resume ID, CONTINUE_LAST → newest .jsonl by mtime at launch, NEW → the file created after launch, excluding a pre-launch snapshot of existing IDs.

stopAndRename() renames exactly that ID in exactly that directory (skipping unchanged titles). The dialog prefills customTitle → slug → empty, never a UUID. The selector's session list is refreshed right after Restart Advanced.

Tests

  • New ClaudeSessionTabPrefillNameTest (6 tests).
  • New store tests for listSessionIds, findLatestSessionId, findSessionIdCreatedAfter, findSession.
  • New controller tests: rename lands in the captured config dir, targets the launched session (not another tab), honors the resume ID, skips unchanged titles, ignores blank names.
  • Red phase verified: the three targeting tests fail against the old implementation; all 1179 tests pass with the fix.

🤖 Generated with Claude Code

…tch dialog

The name entered in the Save & Switch dialog could be written into the
wrong session file, or lost entirely when choosing Restart Advanced.

The controller now captures the session identity at launch instead of
guessing it at stop time:

- the effective CLAUDE_CONFIG_DIR (including profile extra env vars,
  otherwise ~/.claude), so named profiles write into their own store;
- the session ID the running process uses: RESUME_SPECIFIC -> resume ID,
  CONTINUE_LAST -> newest .jsonl by mtime at launch, NEW -> the file
  created after launch (excluding a pre-launch snapshot of existing IDs).

stopAndRename() renames exactly that ID in exactly that directory, and
skips the write when the title is unchanged. The dialog prefills the name
as customTitle -> slug -> empty, never the raw session ID (previously a
UUID was offered for brand-new sessions and then stored as the title).
The session list in the selector is refreshed right after Restart
Advanced so the saved name is visible immediately.

New helpers in ClaudeSessionStore: listSessionIds, findLatestSessionId,
findSessionIdCreatedAfter, findSession, sessionDirForLog. ClaudeProcess
exposes the resolved config dir (configDirFrom/resolveConfigDir/getConfigDir).

Co-Authored-By: Claude Code <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-12T19:26:39.890786Z
We couldn’t complete this review. Comment @codex review to try again.
8d51b3f Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cab2ccdae1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…session

The session identity was fixed at launch, so a conversation switch made
with Claude's own /resume picker left stopAndRename() renaming the
session that was open before the switch.

The controller now re-resolves the active session whenever its ID is
needed: the file the running process wrote last (mtime at or after the
launch) is the active conversation. Tabs sharing a project and config
store publish and claim their own sessions first, so another tab's file
is never mistaken for the one being closed. Session closures release all
claims; only live controllers count as owners.

Also made the mtime-based test fixtures deterministic by setting explicit
file modification times instead of relying on wall-clock writes, which
could tie within one millisecond and make the newest-file lookup
nondeterministic.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@oleg68

oleg68 commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator Author

Both review findings are addressed in 06a850c:

  1. Active ID after an in-process /resume switch — resolveActiveSessionId() no longer returns the launch ID unconditionally. It re-resolves the conversation every time the ID is needed: the session file the running process wrote last (mtime at or after launch) is the active one. Tabs that share a project/config store publish and claim their own sessions first (a new LIVE_SESSIONS / SESSION_CLAIMS registry), so another tab's file is never mistaken for the closed tab's. Claims are released on stop. New regression test stopAndRename_followsSessionSwitchedInProcess fails against the old implementation and passes now.
  2. Nondeterministic mtime fixtures — tests now set explicit filesystem modification times relative to a fixed START_MS / BASE_MTIME instead of using System.currentTimeMillis()-based writes, including the old/new, broken, and fresh fixtures. New findLatestSessionIdWrittenSince tests cover the exclusion of other tabs' files.

Full suite: 1182 tests, 0 failures.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06a850c4a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Oleg Samarin and others added 2 commits November 3, 2026 13:48
Resolving one tab's session could make an unresolved sibling claim the
newer file on its behalf, leaving the resolving tab with no ID and its
entered name dropped on stop.

New sessions now bind to the process that created them: when a prompt is
submitted, a short background poll claims the file written since that
prompt (or since launch for input typed directly into the terminal), and
the 500 ms screen poll does the same. A tab never resolves another tab's
identity: the two-pass publish that forced sibling lookups is removed, so
only a tab's own process-specific evidence claims a session. The strict
lookup excludes sessions already claimed by other live tabs and ignores
files older than the launch.

Regression test newSessionsClaimTheirOwnFileBeforeSiblingGuesses
reproduces the reported ordering (A unresolved, B resolves first) and
fails against the previous implementation.

Co-Authored-By: Claude Code <noreply@anthropic.com>
An unresolved NEW tab scanned its session directory on every 500 ms
screen poll even before the first prompt. Without a plugin-submitted
prompt there is no attribution anchor, so that fallback now runs every
ten ticks (~5 s); with a prompt it still runs on every tick.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e30f26d5ce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Oleg Samarin and others added 2 commits November 3, 2026 14:10
The session-identity poll skipped tabs whose ID was already known, so a
conversation switched inside the TUI left the previous session claimed by
that tab forever: a sibling switching to it excluded its own current file
and resolved to the abandoned one. The PTY waiter also never released the
claims when Claude quit or crashed with the tab still open, keeping an
exited tab an owner indefinitely.

Unified the resolution into one `refreshSessionIdentity()` that re-samples
the newest file written since the launch (or the last plugin prompt),
switches the identity, claims the new session and releases the old claim;
it now runs for known and unknown identities alike (throttled in the
screen poll, every 300 ms for 3 s after a prompt, and synchronously in
`stopAndRename`). The pre-launch snapshot still excludes pre-existing
files while a NEW session is unresolved. The waiter thread reports the
process exit so `LIVE_SESSIONS` and the claims are released, and
`claimedByOthers()` now also ignores owners whose process is not alive.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 86d40bd9a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…races

A NEW tab can resume an existing conversation before it creates a JSONL of
its own. The pre-launch snapshot marked that file ineligible forever, so
the resumed conversation was rejected on every scan and `stopAndRename()`
silently skipped the entered name. The snapshot is now only applied while
the plugin has not submitted a prompt: after a prompt, anything written
since it belongs to this tab, which also covers the resumed file.

The exit callback of the PTY waiter raced with process replacement during
Save & Switch: it passed the owner check, was descheduled, and after the
restart removed the new run from LIVE_SESSIONS and wiped its claims. Exit
cleanup is now generation-based — each start gets a new generation, and
the check and cleanup happen under the same lock that replaces the
process, so a late callback from a stopped run is a no-op.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f9f8cfcaf2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…he EDT

A background identity scan captured the working dir, config dir and launch
time of one process run but published its result blindly: when Save &
Switch replaced the process while the scan was in flight, the old run
could set `activeSessionId` to an ID from the session store it was
scanning, and later resolutions preserved it because the resumed file
predated the new launch. Capturing the ownership generation with the scan
inputs and re-checking it under `processOwnershipLock` at publish time
discards results of a replaced run; the fields are also captured under the
same lock so a scan sees a consistent snapshot.

The periodic scan enumerated the whole session store from the Swing EDT
(every 5 s per tab, every 500 ms while a NEW identity was unresolved after
a prompt), freezing the UI on large histories, and the synchronized method
could make the EDT wait behind a background resolver. The scan now runs on
a shared single-thread worker queued from the screen poll, with at most one
scan per controller in flight; the per-prompt resolver thread is replaced
by an immediate queued scan plus the faster cadence while unresolved.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@oleg68

oleg68 commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

All Codex findings on this PR are fixed. Summary (details in the in-thread replies):

# Finding Fix Verification
1 4453076025 — active ID not updated after an in-process /resume 06a850c: identity re-derived from the newest file written after launch; the claim registry releases the previous session stopAndRename_followsSessionSwitchedInProcess (was red)
2 4453076039 — nondeterministic mtimes in tests 06a850c: explicit fixed setLastModifiedTime instead of System.currentTimeMillis() 1000-run check, no flakes
3 4453563587 — NEW tab claiming a sibling's file d8416cf: removed publishing guessed IDs to siblings; prompt-anchored claim protocol newSessionsClaimTheirOwnFileBeforeSiblingGuesses (was red)
4 4453784534 — stale claims after in-process switches fdd31b8: one refreshSessionIdentity() re-resolves known IDs too, releasing the previous claim inProcessSwitch_releasesTheClaimOnThePreviousSession (was red: a vs c)
5 4453784540 — claims not released when the PTY exits fdd31b8: claude-waiter calls onProcessExited in finally; claimedByOthers() checks process liveness processExit_releasesTheSessionClaims (was red)
6 4454000006 — unresolved NEW session cannot follow an in-process resume f9f8cfc: the pre-launch snapshot applies only until the first plugin prompt newSessionResumingSavedConversation_resolvesAfterPrompt (was red)
7 4454000016 — exit callback racing process replacement f9f8cfc: per-run generation ownership; check and cleanup atomic with replacement staleExitCallback_doesNotReleaseTheReplacementRun (was red)
8 4454204464 — scan result published for a replaced process d0a6e50: generation captured with the scan inputs and re-checked at publish time scanStartedBeforeRestart_doesNotPublishAfterReplacement (was red)
9 4454204479 — periodic scans on the EDT d0a6e50: scans moved to a shared single-thread worker; pollScreenState() only queues full mvn test — 1188 passed

Build: mvn package → netbeans-plugin-claude-code-gui-1.3.70-SNAPSHOT.nbm. Every regression test above was verified red against the previous commit and green after the fix.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d0a6e505de

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Scans of one process run can overlap: the synchronous dialog/stop path and
the background worker both scan. A slow worker that selected session A
could publish after a newer scan had already published B and undo B's
identity and claim, because the single-flight flag only serialized the
scheduled scans. Every scan now reserves a monotonic sequence number under
the ownership lock, and a result is published only if no scan that started
later has published before it.

Claims were keyed by the bare session ID, so a session file copied between
the default store and an isolated profile made a live tab in one store hide
the other store's file: a tab switching to the copy kept its previous
identity and renamed the wrong session. Claims are now keyed by
(config dir, project dir, session ID), and the scan exclusion only considers
claims from the store being scanned.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 421bffc1af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…d the history reload off the EDT

SessionModePanel.reload()/refreshTable() replaced the table model, which
fires fireTableDataChanged() and drops the JTable selection while
RESUME_SPECIFIC stays selected. After "Restart Advanced..." the selector was
refreshed this way, so Open passed a null resume ID and silently launched a
new conversation. The selected row is now remembered by session ID and
restored after the model update, and ClaudeSessionSelectorPanel.onOpen()
rejects RESUME_SPECIFIC when no row is selected.

refreshSessionList() also parsed every candidate JSONL on the Swing EDT during
chat teardown, pausing the IDE for projects with large transcripts. The list
is now loaded on a daemon worker with a request sequence that drops superseded
results, and only the table update runs on the EDT.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3a443f5a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…h release

A background session-list load started for one project/profile could finish
after the user switched to another one and replace both the table and its
stored store with the previous selection. SessionModePanel now applies such a
list only while the directory and config dir still match what the panel shows
(applySessionsIfCurrent), so Open can no longer pair a stale session ID with
the current directory and Rename cannot target the old store.

Session claims used computeIfAbsent() followed by Set.add(), and the release
path removed empty entries with a separate sweep. A claim racing a PTY exit
could land in an owner set that had just been detached from the map, silently
losing it and letting a sibling scan adopt the conversation. Claim membership
and empty-entry cleanup now happen inside compute() for the key, so a claim
cannot be lost.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78ea35d625

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ession mtimes

stopProcess() bumped processGeneration under processOwnershipLock but cleared
processStartedAt / activeConfigDir / promptSubmittedAt afterwards, outside the
lock. A queued identity scan could therefore start between the two and capture
the new generation together with the old run's inputs; its result was accepted
after the stop released the claims, restoring the stopped identity and leaving
the controller claiming both IDs. The generation bump, identity inputs,
LIVE_SESSIONS membership and claim release now happen in one critical section.

findLatestSessionIdWrittenSince() compares file mtimes with the launch/prompt
time recorded in milliseconds, but filesystems with 1-2 s timestamp granularity
round a file created right after the prompt below that cutoff. A NEW session
whose first turn finished in the same timestamp bucket stayed unresolved and
lost the title entered in Save & Switch. The scan now retries with a 2 s slack
when the strict pass finds nothing, restricting the retry to files that did not
exist in the pre-launch snapshot so a conversation touched by an unrelated
process just before the prompt still cannot be adopted.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@oleg68

oleg68 commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 20ee8efb22

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Two tabs can resume the same saved conversation, in which case claimSession()
records both as owners. claimedByOthers() then reported that session to each of
them, so a tab excluded its own active file from the scan; a newer, unrelated
file written by a third tab was resolved instead and stopAndRename() overwrote
the other conversation's title. A key for which this controller is an owner is
now not treated as claimed by others.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a41019fc41

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…e mtime slack to saved conversations

The identity scan treated a file claimed by another live tab as an
unconditional exclusion. When this tab joined another tab's conversation from
its own resume picker, its previous file stopped changing, so no unclaimed file
was written since its prompt and it kept resolving to the conversation it had
left; stopAndRename() then wrote the entered title to the wrong conversation.
When a known identity and a prompt of our own exist and no unclaimed file was
written since that prompt, the scan now follows the newest file written since
the prompt even if another live tab owns it, sharing the ownership instead of
ignoring the switch. While this tab keeps writing its own conversation the
strict pass already finds that file, so another tab's newer file is still not
adopted.

The coarse-timestamp retry previously excluded every pre-launch session ID,
which also rejected a saved conversation resumed in-process (such files exist
before the launch). The retry now only applies that snapshot exclusion when the
tab has not submitted a prompt yet, so the rounded mtime is tolerated for
resumed saved conversations while pre-launch files still cannot be adopted
before a prompt.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e805f4ac36

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The recorder's menu context and the tracked conversation could still
outlive the event they described, so Save & Switch could either swallow a
real prompt or name the conversation a /resume had left behind.

- The ARROW-digit worker navigates out of a focused text editor so the
  digit answer selects an ordinary option, but it never recorded the
  forwarded focus: a digit typed in the terminal kept counting as editor
  text and the prompt after it was swallowed with the menu. Each arrow is
  now written and tracked inside one terminalInputLine section.
- Every answer worker's submitting key (Enter, or Right for checkbox
  menus) was written outside the recorder lock and the menu retired only
  afterwards, so a prompt typed right after the key was classified
  against the dismissed menu. writePtyAnsweringMenu() writes the key and
  retires the menu in one lock section, and all workers use it.
- The guard for an unresolved /resume switch only covered identities
  established at launch. A NEW tab that had resolved its conversation by
  prompt evidence was renamed after the picker left it behind: the guard
  now protects whatever identity is still tracked while the switch has
  not been followed by a submission.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@oleg68

oleg68 commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d38267dd96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Two ways the session name could still land in, or be composed from, the
wrong conversation.

- writePtyAnsweringMenu() retired the menu under the recorder lock but
  kept the line typed into it while the answer worker paused. A text
  option's answer or a picker's search filter then prefixed the next
  prompt sent from the panel, and the composed text was published as
  exact evidence of a prompt the conversation never received. The submit
  now discards the menu-owned line and cursor together with the
  retirement.
- The rename-ambiguity check excluded every file another live tab had
  launched on. A launch or resume does not prove which process recorded
  this run's submission — it may be the file this run wrote — so the
  exclusion hid a competing file and let the stop rename the tab's guess.
  Only the tracked candidate and the launch conversation stay out of the
  competition now; every other file carrying the submission keeps the
  identity ambiguous.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@oleg68

oleg68 commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@oleg68

oleg68 commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@oleg68

oleg68 commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@oleg68

oleg68 commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b15b4c598f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ty gaps

Require an evidence-based scan result from after a conversation switch before
the stop-time guard lets a rename through: a prompt submitted after the switch
proves nothing about where the picker went, since the destination may be a live
sibling's file no evidence pass can adopt, and the same prompt also lands in the
launch conversation when the selection never took effect. The guard now tracks
the anchor of the scan result that identified a conversation written since the
switch instead of comparing submission times.

Publish the line the answer worker's Enter submits when terminal input already
answered the choice menu during the worker's pause: the TUI's input line is a
fresh prompt draft by then, so discarding it dropped the only evidence that
follows the conversation the prompt was written to.

Roll back a pending /resume switch when its picker is cancelled: opening the
picker switches nothing, so the evidence the command replaced (conversation
switch, prompt text and time, external-input time) is snapshotted at the
submission and restored by every cancellation path — terminal Esc (consumed by
the next write or at stop time), panel cancel, and the GUI send that consumes
the pending Esc. A confirmed selection disarms the snapshot.

Regression tests: aPromptBeforeTheDestinationRecordDoesNotUnblockTheRename,
aSwitchResolvedByTheDestinationRecordStaysRenamable,
theWorkersEnterAfterAnEarlyMenuAnswerIsRecorded,
aCancelledPickerKeepsTheLaunchConversationRenamable,
aCancelledPickerRestoresThePromptEvidence.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@oleg68

oleg68 commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

The /resume Enter moves externalInputAt before the command branch arms the
picker switch, so the snapshot the cancellation restored held the command's
own time instead of the pre-command one. After a cancel-restore the earlier
prompt then looked stale to the stop-time lookup, which skipped its text
evidence and the cutoff it judges competition by.

Captured the cutoff inside the recorder lock before any input is applied and
passed it to armPickerSwitch.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57d5579d3b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +5784 to +5786
ChoiceMenuModel escapedMenu = openChoiceMenu();
terminalEscapeOnMenu = escapedMenu != null;
terminalEscapeOnPicker = isConversationPicker(escapedMenu);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recognize picker cancellation before the menu is published

If the user opens /resume and presses Esc before the 400/500 ms screen-detection timers publish the picker, openChoiceMenu() is still null here. Both escape flags remain false, so neither subsequent input nor stopAndRename() calls restorePickerSwitch(). Consequently, conversationSwitchAt remains set even though the original conversation is still active. If the user then enters a name in Save & Switch without submitting another persisted prompt, identityRemainsAmbiguous() skips the rename and the entered name is lost. Cancellation handling needs to account for the already-armed picker state rather than requiring the menu to have reached the model.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@/tmp/pr206/r4.md

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@/tmp/pr206/r5.md

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in d4b156e.

You are right: the pending /resume admission is armed long before the screen poll publishes the picker, so an Esc pressed in that window reached appendTerminalInput() with openChoiceMenu() == null. Both escape flags stayed false, the cancellation was never consumed, and identityRemainsAmbiguous() kept treating the conversation as switched — the entered name was dropped.

appendTerminalInput() now classifies an Esc as "cancelled picker" when no menu is published but a picker admission is pending (escapedMenu == null && pickerSwitchPending), so the existing consumption paths (next write, stopAndRename() while the Esc is still pending) roll the switch evidence back.

Regression test: aPickerCancelledBeforeItIsPublishedKeepsTheLaunchConversationRenamable — submits /resume, sends Esc without ever publishing a menu to the model, then stops with a name. It fails before the change (expected: <A-title> but was: <null>) and passes after it. ClaudeSessionControllerTest 246/246, full mvn test 1403 tests with 0 failures.

The screen detector publishes the /resume picker a few hundred milliseconds
after the command, so an Esc pressed right away arrives while openChoiceMenu()
is still null. The Esc was then not classified as a menu cancellation, the
pending switch was never rolled back, and the stop treated the conversation as
switched and dropped the entered name.

An Esc with no published menu is now classified as the cancelled picker while
a /resume admission is pending.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@oleg68

oleg68 commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

The panel's Cancel button sends its own Esc to the PTY, bypassing the terminal
recorder, so the pending /resume switch was only rolled back when the screen
poll had already published the picker. The same unpublished-menu window
therefore lost the entered name when the user cancelled from the panel instead
of the keyboard.

The cancel now treats a pending /resume admission as the picker too.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce5b01cfb0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (enterOnAnsweredMenu) {
draft = terminalInputLine.toString().trim();
draftExact = !terminalHistoryRecall && !terminalCompletionEdited;
resumeDraft = draftExact && isResumeCommand(draft);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recognize /clear when a menu worker submits a fresh line

If terminal Enter answers an ordinary menu during an answer worker's pause and the worker's final Enter submits a newly typed /clear, this recovery path records /clear as a human prompt without advancing conversationSwitchAt; only /resume receives command handling. A subsequent recalled or Tab-completed prompt in a launch-known tab therefore skips the unheld-destination lookup in refreshSessionIdentity(). An isolated controller/store replay with another live writer on launch conversation a consequently renames a, even though the new clear destination b is strictly newer and contains the actual prompt. Apply the existing isConversationCommand() handling here too, arming a cancellable picker only for /resume.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@/tmp/pr206/r6.md

Comment on lines +3862 to +3865
ChoiceMenuModel menu = openChoiceMenu();
if (isConversationPicker(menu) || (menu == null && pickerSwitchPending)) {
restorePickerSwitch();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retire a published picker when the prompt panel cancels it

With Hide menu and an already-published /resume picker, cancelling from the prompt panel restores the previous evidence but leaves the model menu and its recorded search draft active; the plugin-written Esc bypasses the input listener. If the next panel prompt completes before that stale menu is cleared, completeGuiSend() mistakes its Enter for a picker selection, drops the real prompt evidence, and sets conversationSwitchAt again. An isolated recorder/store replay then makes even a single-file launch ambiguous and loses the Save & Switch name. The new unpublished-picker cancellation test does not exercise this published-menu state; retire the menu and discard its draft atomically with the cancelling write.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@/tmp/pr206/r7.md

if (resumeDraft) {
scheduleIdentityRefresh();
} else {
rememberSentPrompt(draft, submittedAt, draftExact);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Publish recovered prompt evidence under the recorder lock

When terminal input has already answered the picker, the worker's final Enter can submit a fresh prompt, but this publishes its evidence after releasing terminalInputLine. A terminal writer waiting on that lock can submit a newer prompt first, after which this call overwrites lastPromptText and promptSubmittedAt with the older draft while externalInputAt retains the newer time. A two-writer replay of the new early-answer recovery path reproduces that reversal: both prompt-evidence branches fail their promptAt >= externalAt guard, leaving a switch into a sibling's live conversation unresolved and suppressing the requested rename. Publish the captured evidence inside the same critical section as its Enter and recorder reset.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@/tmp/pr206/r8.md

…wer workers

- writePtyAnsweringMenu: a /clear submitted by the worker's Enter is a
  conversation switch like /resume, so it is no longer recorded as prompt text;
  only /resume arms the cancellable picker.
- The recovered prompt evidence is now published inside the recorder critical
  section together with its Enter and recorder reset, and only when it is not
  older than the evidence already recorded, so a terminal writer that submits
  concurrently keeps its newer text and time.
- cancelPrompt: the plugin-written Esc bypasses the recorder, so a published
  menu is now retired and its search text discarded atomically with the
  cancelling write; otherwise the next panel send's Enter was read as a picker
  selection and its prompt evidence was dropped.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@oleg68

oleg68 commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 10406ffa0c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (isConversationPicker(menu) || (menu == null && pickerSwitchPending)) {
restorePickerSwitch();
}
if (menu != null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear search text when cancelling an unpublished picker

If /resume opens and the user types a search filter before the screen poll publishes the picker, panel Cancel restores the previous switch state but this guard skips clearing the filter from terminalInputLine. Fresh evidence beyond the unpublished-picker cancellation fix: an extracted replay of /resume<Enter>, filter, panel Cancel, then panel hello publishes exact filterhello, although the TUI submits only hello. This breaks the actual conversation's prompt match and can redirect Save & Switch to an unrelated transcript containing filterhello, even when the real conversation is newer. Discard the picker-owned draft when cancelling a pending unpublished picker too.

Useful? React with 👍 / 👎.

// The menu is gone, so this Enter submits the draft on the
// fresh line — whether it was typed after a terminal Enter
// that answered the menu or after a digit the panel sent.
rememberSentPrompt(draftLine, submittedAt, draftExact);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle conversation commands in recovered panel submissions

With Hide menu, terminal Enter can answer an ordinary menu during a panel send's pause, after which the user can type /clear and have the panel's delayed Enter submit it here. Fresh evidence beyond the writePtyAnsweringMenu() command fix: this separate recovery path still records /clear as prompt text and leaves conversationSwitchAt == 0 in an extracted current-method replay. A subsequent recalled or Tab-completed prompt in a launch-known tab therefore skips the unheld-destination lookup; if another live tab appends a user record to the launch conversation, Save & Switch can rename that old file even though the cleared conversation is newer. Apply the existing conversation-command handling to recovered panel drafts as well.

Useful? React with 👍 / 👎.

The panel cancel discards the picker's search text only when the menu reached
the model. When the cancel lands before the screen poll publishes the picker,
the digits typed into it stay in the recorder and would prefix the next
prompt, so a pending /resume admission now drops the draft too.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d51b3f652

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1501 to +1502
if (findPromptBearingSession(dir, configDir, since, exclude, null,
promptAt, promptText) != null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve ambiguity when the competitor scan hits its limit

When two NEW tabs submit the same prompt, a tab can temporarily track its sibling's file before its own JSONL appears. If eight unrelated files subsequently have newer mtimes, findPromptBearingSession() examines only those eight and this check misses the real competing file; the relaxed pass has the same cap. An isolated resolver/store replay makes ambiguity change from true to false and stopAndRename("A-title") rename the sibling's b.jsonl while the matching a.jsonl is still present. Search competitors without the discovery cap, or fail closed when the search is truncated.

Useful? React with 👍 / 👎.

Comment on lines +3845 to +3846
confirmPickerSwitch();
conversationSwitchAt = System.currentTimeMillis();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Confirm picker switches only after the submitting key is written

If the user selects a different GUI row in /resume and presses terminal Esc during the arrow worker's navigation delay, this has already cleared pickerSwitchPending, so cancellation cannot restore the pre-picker evidence. Fresh evidence beyond the earlier picker-cancellation fixes: an isolated replay using the current ARROW:1 worker leaves conversationSwitchAt set after Esc and makes stopAndRename() discard the entered name, whereas cancelling before starting that worker correctly renames the original conversation. The worker's later Enter submits an empty ordinary line, not a picker selection, so it supplies no destination evidence. Keep cancellation reversible until the submitting Enter actually answers the still-open picker.

Useful? React with 👍 / 👎.

@oleg68

oleg68 commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@oleg68
oleg68 merged commit a688d17 into nbplugins:main Nov 13, 2026
3 checks passed
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