Skip to content

bug: a session that switches into a git worktree is remembered in the directory it was launched in #1878

Description

@shohei-sasaki835

A session that switches into a git worktree while it is running keeps being remembered in the
directory its cell was launched in, so after a restart it comes back as a session of the parent
repository and the worktree it actually worked in has no cell and no resume row. The live directory
is already computed on every hook (resolveHookCwd); it just never reaches the record that
outlives the process. Plan below, one function plus one call.

What happens

  1. Open a cell in a repository and start Claude.
  2. Have it move into a git worktree of that repository within the same session (Claude Code's own
    worktree switch does this — the agent's working directory changes, the session does not restart).
  3. Restart MulmoTerminal, or reboot, which takes tmux with it.
  4. The conversation is listed under the parent repository. Nothing offers it for the worktree
    directory it was working in.

Expected: it comes back as a session of the directory it was last running in.

Why

markDevTerminalSession (server/session/registry.ts) is the only writer of
dev-terminal-cwds.json, and the directory it is handed is the one the CELL was launched or
reattached in — all the spawn path can know. Nothing tells it that the agent moved.

That record is load-bearing after a restart: server/session/surviving-sessions.ts says it in its
own comment — "the remembered directories are the only thing a session from a PREVIOUS run can be
named by" (#1021) — and cwdOf there, cwdOfSession in server/index.ts and pickSessionCwd in
server/session/session-cwd.ts all fall back to it once the PTY is gone. session-cwd.ts and
ws-routes.ts:961 both describe that value as "where the session really runs", which is exactly
what it stops being.

Measured on one machine: 11 of 114 recorded directories disagreed with the last cwd in the
session's own transcript, and 10 of the 11 were worktrees.

The value is already there. server/routes/hook-routes.ts:247 computes it on every hook:

const cwd = resolveHookCwd(body.cwd, entry?.cwd);

and resolveHookCwd's own comment (server/session/activity-hook.ts) explains why the payload
wins: "the PTY table only holds the dir the session was SPAWNED in — that value goes stale the
moment the session cds". Every other consumer of a hook already uses it. The persisted record
does not.

Proposed plan

Two files.

  • server/session/registry.ts — export a narrow noteLiveSessionCwd(id, cwd) that calls the
    existing private rememberSessionCwd. Deliberately not markDevTerminalSession: that also
    enrols the id as a grid session, and any well-formed uuid may be posted to /api/hook. The guard
    is devTerminalSessions.has(id), so only a session something already enrolled can write — which
    includes a survivor, because that set is persisted too. rememberSessionCwd already skips the
    append when the value is unchanged, so a hook firing many times a turn costs nothing.
  • server/routes/hook-routes.ts — one call after line 247: if (cwd) noteLiveSessionCwd(sessionId, cwd);

Appending is enough to correct an entry: parseDevTerminalCwds takes the last entry for an id.

Open questions for the maintainers:

  • Only Claude reports hooks. codex / antigravity / grok sessions cannot be corrected this way;
    their directory lives in the per-agent conversation log instead. Should this issue stay
    claude-only, or is the conversation-log side wanted in the same change?
  • A subdirectory is also a move. An agent that works in <repo>/packages/api will be recorded
    there rather than at the repository root. That is what "where it really runs" means and the
    git-backed features resolve upwards anyway, but it does change which directory a survivor is
    grouped under, so it is a product call rather than an implementation detail.

I have a working patch of exactly the above, if it is useful as a starting point: 28 added lines
across the two files, plus two specs (noteLiveSessionCwd's rule, and the wiring at the route —
the latter fails when the one call is removed). The full server suite passes with it
(438 files / 6685 tests), as do tsc -p tsconfig.server.json, tsc -p tsconfig.test-server.json
and eslint on the touched files. I am not sending a pull request — per CONTRIBUTING.md the plan is
what I am proposing.

Environment

  • MulmoTerminal 4.10.1, started with npx mulmoterminal
  • macOS 26.6.2, arm64, Node v24.19.0, zsh
  • tmux 3.7b, Claude Code 2.1.247
  • Browser: Chrome

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions