mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
fix: refuse a case folder move while a session is still being created in it (release review)
The rename lock is checked once, in the Session constructor, but a session
reaches the server's map (where the rename route builds its list of open
sessions) only later, after the create awaits the owner's layout lock and a
fresh settings read (and, for POST /api/sessions, inferLocalCaseName). A
rename in that gap neither listed nor stopped the session, moved the folder,
and the create then registered a session whose pane starts with
`cd "<old path>"`, skipping the 409 requiresStop / stopSessions guarantee.
- case-rename.ts keeps a record of sessions constructed but not yet
registered (`noteCaseSessionStarting` / `caseSessionSettled` /
`isCaseSessionStarting`), with a 30 s TTL for one that never registers
(a failed create, a one-shot prompt), swept on every note and check.
- The Session constructor records itself (local sessions only, as the
route ignores remote ones); registerSessionWithLayout()'s register
callback, the boot-restore registration and stop() settle it.
- The rename route answers 409 CONFLICT with renameCode BUSY ("A session is
starting in this case") for a folder move while one is recorded at or
under the folder. The check runs in the same tick that takes the lock, so
every interleaving is either refused here or by the constructor. An
alias-only rename is not refused.
A create whose path was validated before the move but whose Session is
constructed after it is still the ordinary check-then-use race of any
create path; CLAUDE.md now says so instead of "nothing starts mid-move".
Tests: unit (record, settle, stop, TTL), route (BUSY, then requiresStop once
registered; alias-only unaffected) and an end-to-end WebServer test that
holds the create between constructor and registration (it fails without
the route check: the folder moves under the session).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
40158ce1ef
commit
f8029c2a9e
10 files changed
+238
-7
No files matched your search
@@ -55,6 +55,7 @@ import { generateClaudeMd } from '../../templates/claude-md.js';
|
||||
import { prepareNewCasePath } from '../case-path.js';
|
||||
import {
|
||||
CaseRenameError,
|
||||
isCaseSessionStarting,
|
||||
moveCaseFolder,
|
||||
moveClaudeHistory,
|
||||
renameInOrder,
|
||||
@@ -1848,6 +1849,14 @@ export function registerCaseRoutes(
|
||||
if (open.some((s) => !canAccessOwned(user, s.owner))) {
|
||||
return fail(403, ApiErrorCode.FORBIDDEN, "Another user's session has this case open");
|
||||
}
|
||||
// A session being created in the folder is not in ctx.sessions yet, so it could be neither listed nor
|
||||
// stopped: it would start in the old path after the move. Checked in the same tick that takes the lock
|
||||
// below (nothing is awaited in between), and the Session constructor refuses the folder from then on.
|
||||
if (moveFolder && isCaseSessionStarting(oldPath)) {
|
||||
return fail(409, ApiErrorCode.CONFLICT, 'A session is starting in this case. Try again in a moment.', {
|
||||
renameCode: 'BUSY',
|
||||
});
|
||||
}
|
||||
if (open.length > 0 && !body.stopSessions) {
|
||||
return fail(
|
||||
409,
|
||||
|
||||
@@ -242,6 +242,7 @@ import {
|
||||
} from './routes/index.js';
|
||||
import { isLostWebviewFrameNavigation } from './webview-proxy.js';
|
||||
import { CronService } from '../cron/cron-service.js';
|
||||
import { caseSessionSettled } from '../case-rename.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -806,6 +807,8 @@ export class WebServer extends EventEmitter {
|
||||
private async registerSessionWithLayout(session: Session, placement?: SessionPlacementHint): Promise<TabLayout> {
|
||||
return this.tabLayouts.sessionCreated(ownerLayoutKey(session.owner), session.id, placement, () => {
|
||||
this.sessions.set(session.id, session);
|
||||
// In the map now, so a case rename lists (and on request stops) it instead of refusing the move.
|
||||
caseSessionSettled(session);
|
||||
return () => {
|
||||
if (this.sessions.get(session.id) === session) this.sessions.delete(session.id);
|
||||
};
|
||||
@@ -3957,6 +3960,7 @@ export class WebServer extends EventEmitter {
|
||||
}
|
||||
|
||||
this.sessions.set(session.id, session);
|
||||
caseSessionSettled(session);
|
||||
await this.setupSessionListeners(session);
|
||||
|
||||
// Auto-attach PTY to the surviving tmux session immediately.
|
||||
|
||||
Reference in new issue
Block a user