fix: COD-24 stabilize remote host sessions

This commit is contained in:
Saqeb Akhter
2026-07-09 09:06:06 -04:00
committed by Aamer Akhter
parent 568d93efb0
commit 26e78daf58
11 changed files with 512 additions and 30 deletions
+28 -4
View File
@@ -95,21 +95,26 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
const remoteHostMap = new Map(remoteHosts.map((host) => [host.id, host]));
for (const remoteCase of await readRemoteCases(CODEMAN_CONFIG_DIR)) {
const host = remoteHostMap.get(remoteCase.hostId);
if (!host || existingNames.has(remoteCase.name) || !SAFE_CASE_NAME.test(remoteCase.name)) continue;
if (!host || !SAFE_CASE_NAME.test(remoteCase.name)) continue;
existingNames.add(remoteCase.name);
cases.push({
const remoteCaseInfo: CaseInfo = {
name: remoteCase.name,
path: remoteDisplayPath({ username: host.username, host: host.host, path: remoteCase.remotePath }),
hasClaudeMd: false,
location: 'remote',
remote: {
hostId: host.id,
hostLabel: host.label,
host: host.host,
username: host.username,
path: remoteCase.remotePath,
},
});
};
const existingIndex = cases.findIndex((item) => item.name === remoteCase.name);
if (existingIndex === -1) {
cases.push(remoteCaseInfo);
} else {
cases[existingIndex] = remoteCaseInfo;
}
}
// Sort by persisted caseOrder from settings.json
@@ -341,6 +346,25 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name');
}
const remoteCases = await readRemoteCases(CODEMAN_CONFIG_DIR);
const remoteCase = remoteCases.find((item) => item.name === name);
if (remoteCase) {
const host = (await readRemoteHosts(CODEMAN_CONFIG_DIR)).find((item) => item.id === remoteCase.hostId);
if (!host) return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Remote host not found');
return {
name,
path: remoteDisplayPath({ username: host.username, host: host.host, path: remoteCase.remotePath }),
hasClaudeMd: false,
location: 'remote',
remote: {
hostId: host.id,
host: host.host,
username: host.username,
path: remoteCase.remotePath,
},
};
}
const casePath = await resolveCasePath(name);
if (!existsSync(casePath)) {
+31 -17
View File
@@ -1293,24 +1293,36 @@ export function registerSessionRoutes(
}
}
// Resolve case path: check linked-cases registry first, then fall back to CASES_DIR.
// This mirrors the behaviour of resolveCasePath() in case-routes so that linked
// external project directories are honoured by quick-start just like regular case routes.
let linkedCases: Record<string, string> = {};
try {
const raw = await fs.readFile(LINKED_CASES_FILE, 'utf-8');
linkedCases = JSON.parse(raw);
} catch {
// File missing or unparseable — treat as empty registry
}
const linkedCasePath = linkedCases[caseName];
const casePath = linkedCasePath || validatePathWithinBase(caseName, CASES_DIR);
if (!casePath) {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case path');
let remote = undefined;
let linkedCasePath: string | undefined;
let casePath: string | null = null;
const remoteCases = await readRemoteCases(CODEMAN_CONFIG_DIR);
const remoteCase = remoteCases.find((item) => item.name === caseName);
if (remoteCase) {
const host = (await readRemoteHosts(CODEMAN_CONFIG_DIR)).find((item) => item.id === remoteCase.hostId);
if (!host) return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Remote host not found');
casePath = remoteCase.remotePath;
remote = toSessionRemote(host, remoteCase);
} else {
// Resolve case path: check linked-cases registry first, then fall back to CASES_DIR.
// This mirrors the behaviour of resolveCasePath() in case-routes so that linked
// external project directories are honoured by quick-start just like regular case routes.
let linkedCases: Record<string, string> = {};
try {
const raw = await fs.readFile(LINKED_CASES_FILE, 'utf-8');
linkedCases = JSON.parse(raw);
} catch {
// File missing or unparseable — treat as empty registry
}
linkedCasePath = linkedCases[caseName];
casePath = linkedCasePath || validatePathWithinBase(caseName, CASES_DIR);
if (!casePath) {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case path');
}
}
// Create case folder and CLAUDE.md if it doesn't exist (only for non-linked cases)
if (!existsSync(casePath)) {
// Create case folder and CLAUDE.md if it doesn't exist (only for non-linked, non-remote cases)
if (!remote && !existsSync(casePath)) {
try {
mkdirSync(casePath, { recursive: true });
mkdirSync(join(casePath, 'src'), { recursive: true });
@@ -1343,6 +1355,7 @@ export function registerSessionRoutes(
mode !== 'opencode' &&
mode !== 'codex' &&
mode !== 'gemini' &&
!remote &&
envOverrides &&
Object.keys(envOverrides).length > 0
) {
@@ -1379,12 +1392,13 @@ export function registerSessionRoutes(
geminiConfig: mode === 'gemini' ? geminiConfig : undefined,
envOverrides,
effort,
remote,
tmuxHistoryLimit: qsTerminalHistoryConfig.tmuxHistoryLimit,
});
// Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting
// so the initial state already has the phrase configured (only if globally enabled)
if (mode === 'claude' && ctx.store.getConfig().ralphEnabled) {
if (mode === 'claude' && !remote && ctx.store.getConfig().ralphEnabled) {
autoConfigureRalph(session, casePath, ctx);
if (!session.ralphTracker.enabled) {
session.ralphTracker.enable();