fix: address follow-up review regressions

This commit is contained in:
harshitsinghbhandari 2026-04-19 19:46:12 +05:30
parent 330fe7e63c
commit 6b15e68fc3
3 changed files with 29 additions and 4 deletions

View File

@ -6,6 +6,7 @@ import {
openSync,
closeSync,
constants,
statSync,
} from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
@ -24,6 +25,7 @@ const STATE_FILE = join(STATE_DIR, "running.json");
const STATE_LOCK_FILE = join(STATE_DIR, "running.lock");
const STARTUP_LOCK_FILE = join(STATE_DIR, "startup.lock");
const MAX_LOCK_AGE_MS = 30 * 60 * 1000;
const UNPARSEABLE_LOCK_GRACE_MS = 5_000;
interface LockMetadata {
pid: number;
@ -62,10 +64,20 @@ function readLockMetadata(lockFile: string): LockMetadata | null {
}
function isStaleLockOwner(owner: LockMetadata): boolean {
if (isProcessAlive(owner.pid)) return false;
const acquiredAt = Date.parse(owner.acquiredAt);
return Number.isFinite(acquiredAt) && Date.now() - acquiredAt > MAX_LOCK_AGE_MS;
}
function isStaleUnparseableLock(lockFile: string): boolean {
try {
const mtimeMs = statSync(lockFile).mtimeMs;
return Date.now() - mtimeMs > UNPARSEABLE_LOCK_GRACE_MS;
} catch {
return false;
}
}
/** Try to create the lockfile atomically. Returns a release function on success, null on failure. */
function tryAcquire(lockFile: string): (() => void) | null {
try {
@ -110,7 +122,8 @@ async function acquireLock(
if (release) return release;
const owner = readLockMetadata(lockFile);
if (owner && (isStaleLockOwner(owner) || !isProcessAlive(owner.pid))) {
if ((!owner && isStaleUnparseableLock(lockFile))
|| (owner && (isStaleLockOwner(owner) || !isProcessAlive(owner.pid)))) {
try { unlinkSync(lockFile); } catch { /* ignore */ }
const retryRelease = tryAcquire(lockFile);
if (retryRelease) return retryRelease;

View File

@ -394,7 +394,7 @@ describe("API Routes", () => {
expect(mockSessionManager.listCached).toHaveBeenCalledWith("my-app");
});
it("omits dead orchestrators from project-scoped orchestrator payloads when none are live", async () => {
it("keeps dead orchestrators as the fallback project-scoped payload when none are live", async () => {
const deadLifecycle = createInitialCanonicalLifecycle("orchestrator", new Date("2026-04-19T11:00:00.000Z"));
deadLifecycle.session.state = "terminated";
deadLifecycle.session.reason = "runtime_missing";
@ -423,7 +423,9 @@ describe("API Routes", () => {
const data = await res.json();
expect(data.orchestratorId).toBeNull();
expect(data.orchestrators).toEqual([]);
expect(data.orchestrators).toEqual([
{ id: "my-app-orchestrator-0", projectId: "my-app", projectName: "My App" },
]);
expect(data.sessions).toEqual([]);
});

View File

@ -52,6 +52,16 @@ function listLiveDashboardOrchestrators(
);
}
function listPreferredProjectOrchestrators(
sessions: Parameters<typeof listDashboardOrchestrators>[0],
projects: Parameters<typeof listDashboardOrchestrators>[1],
) {
const liveOrchestrators = listLiveDashboardOrchestrators(sessions, projects);
return liveOrchestrators.length > 0
? liveOrchestrators
: listDashboardOrchestrators(sessions, projects);
}
export async function GET(request: Request) {
const correlationId = getCorrelationId(request);
const startedAt = Date.now();
@ -69,7 +79,7 @@ export async function GET(request: Request) {
const coreSessions = await sessionManager.listCached(requestedProjectId);
const visibleSessions = filterProjectSessions(coreSessions, projectFilter, config.projects);
const orchestrators = requestedProjectId
? listLiveDashboardOrchestrators(visibleSessions, config.projects)
? listPreferredProjectOrchestrators(visibleSessions, config.projects)
: listDashboardOrchestrators(visibleSessions, config.projects);
const orchestratorId = requestedProjectId
? selectPreferredOrchestratorId(visibleSessions, config.projects)