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
- Open a cell in a repository and start Claude.
- 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).
- Restart MulmoTerminal, or reboot, which takes tmux with it.
- 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
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 thatoutlives the process. Plan below, one function plus one call.
What happens
worktree switch does this — the agent's working directory changes, the session does not restart).
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 ofdev-terminal-cwds.json, and the directory it is handed is the one the CELL was launched orreattached 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.tssays it in itsown comment — "the remembered directories are the only thing a session from a PREVIOUS run can be
named by" (#1021) — and
cwdOfthere,cwdOfSessioninserver/index.tsandpickSessionCwdinserver/session/session-cwd.tsall fall back to it once the PTY is gone.session-cwd.tsandws-routes.ts:961both describe that value as "where the session really runs", which is exactlywhat it stops being.
Measured on one machine: 11 of 114 recorded directories disagreed with the last
cwdin thesession's own transcript, and 10 of the 11 were worktrees.
The value is already there.
server/routes/hook-routes.ts:247computes it on every hook:and
resolveHookCwd's own comment (server/session/activity-hook.ts) explains why the payloadwins: "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 recorddoes not.
Proposed plan
Two files.
server/session/registry.ts— export a narrownoteLiveSessionCwd(id, cwd)that calls theexisting private
rememberSessionCwd. Deliberately notmarkDevTerminalSession: that alsoenrols the id as a grid session, and any well-formed uuid may be posted to
/api/hook. The guardis
devTerminalSessions.has(id), so only a session something already enrolled can write — whichincludes a survivor, because that set is persisted too.
rememberSessionCwdalready skips theappend 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:
parseDevTerminalCwdstakes the last entry for an id.Open questions for the maintainers:
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?
<repo>/packages/apiwill be recordedthere 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.jsonand eslint on the touched files. I am not sending a pull request — per CONTRIBUTING.md the plan is
what I am proposing.
Environment
npx mulmoterminal