diff --git a/etc/servers/work-server/src/services/test-server-deployment-service.ts b/etc/servers/work-server/src/services/test-server-deployment-service.ts index 1a775dc..91bb8c1 100644 --- a/etc/servers/work-server/src/services/test-server-deployment-service.ts +++ b/etc/servers/work-server/src/services/test-server-deployment-service.ts @@ -193,7 +193,8 @@ function normalizeTestServerDeploymentSnapshot(value: unknown): TestServerDeploy export async function readTestServerDeploymentState(): Promise { try { const raw = await readFile(getTestServerDeploymentStatePath(), 'utf8'); - return normalizeTestServerDeploymentSnapshot(JSON.parse(raw)); + const snapshot = normalizeTestServerDeploymentSnapshot(JSON.parse(raw)); + return await resolveStaleRunningTestDeployment(snapshot); } catch { return null; } @@ -209,6 +210,62 @@ async function clearTestServerDeploymentState() { await rm(getTestServerDeploymentStatePath(), { force: true }).catch(() => undefined); } +function buildStaleTestServerDeploymentFailure(snapshot: TestServerDeploymentSnapshot) { + const stalledAt = snapshot.updatedAt ?? snapshot.startedAt; + const stalledLabel = stalledAt ? `마지막 상태 갱신 ${stalledAt}` : '상태 갱신 시각 확인 불가'; + return trimPreview(`TEST 배포 상태가 오래 갱신되지 않았고 잠금 파일도 없어 중단된 배포로 처리했습니다. ${stalledLabel}`, 500) + ?? 'TEST 배포 상태가 오래 갱신되지 않아 중단된 배포로 처리했습니다.'; +} + +async function finalizeStaleRunningTestDeployment(snapshot: TestServerDeploymentSnapshot) { + const failureMessage = buildStaleTestServerDeploymentFailure(snapshot); + const now = new Date().toISOString(); + const activeStep = snapshot.steps.find((step) => step.status === 'running')?.key; + + snapshot.status = 'failed'; + snapshot.phase = 'failed'; + snapshot.summary = buildTestServerDeploymentSummary('failed'); + snapshot.completedAt = now; + snapshot.updatedAt = now; + snapshot.lastError = failureMessage; + + if (activeStep) { + updateTestServerDeploymentStep(snapshot, activeStep, 'failed', failureMessage); + } + + await writeTestServerDeploymentState(snapshot); + return snapshot; +} + +async function resolveStaleRunningTestDeployment(snapshot: TestServerDeploymentSnapshot) { + if (snapshot.status !== 'running') { + return snapshot; + } + + const freshnessSource = snapshot.updatedAt ?? snapshot.startedAt; + if (!freshnessSource) { + return snapshot; + } + + const staleForMs = Date.now() - Date.parse(freshnessSource); + if (!Number.isFinite(staleForMs) || staleForMs < TEST_SERVER_DEPLOYMENT_LOCK_STALE_MS) { + return snapshot; + } + + const lockPath = getTestServerDeploymentLockPath(); + const lockStat = await stat(lockPath).catch(() => null); + + if (lockStat?.isFile()) { + const lockFreshnessSource = normalizeDateTimeValue(lockStat.mtime.toISOString() ?? null); + if (lockFreshnessSource && Date.now() - Date.parse(lockFreshnessSource) < TEST_SERVER_DEPLOYMENT_LOCK_STALE_MS) { + return snapshot; + } + } + + await rm(lockPath, { force: true }).catch(() => undefined); + return finalizeStaleRunningTestDeployment(snapshot); +} + async function acquireTestServerDeploymentLock() { const lockPath = getTestServerDeploymentLockPath(); await mkdir(path.dirname(lockPath), { recursive: true }); diff --git a/src/app/main/pages/ChatSharePage.tsx b/src/app/main/pages/ChatSharePage.tsx index 955a942..296894e 100644 --- a/src/app/main/pages/ChatSharePage.tsx +++ b/src/app/main/pages/ChatSharePage.tsx @@ -3552,6 +3552,17 @@ export function ChatSharePage() { const nextCustomContextContent = editingRoomCustomContextContent.trim(); const shouldPersistRoomDefaultContextIds = !areStringListsEqual(normalizedDefaultContextIds, inheritedDefaultContextIds); const shouldPersistRoomCustomContext = Boolean(nextCustomContextTitle || nextCustomContextContent); + const currentRoomDefaultContextIds = activeRoomContextSettings?.defaultContextIds ?? []; + const currentRoomCustomContextTitle = activeRoomContextSettings?.customContextTitle?.trim() ?? ''; + const currentRoomCustomContextContent = activeRoomContextSettings?.customContextContent?.trim() ?? ''; + const currentRoomCodexParticipants = activeRoomContextSettings?.codexParticipants ?? []; + const shouldSaveRoomContextSettings = + canManageSharedRoomSettings + && ( + !areStringListsEqual(normalizedDefaultContextIds, currentRoomDefaultContextIds) + || nextCustomContextTitle !== currentRoomCustomContextTitle + || nextCustomContextContent !== currentRoomCustomContextContent + ); const normalizedRoomTitle = editingRoomTitle.trim(); const normalizedAccessPin = editingRoomAccessPin.trim(); const currentHasAccessPin = snapshot?.share.hasAccessPin === true; @@ -3596,23 +3607,26 @@ export function ChatSharePage() { await ensureRoomNotificationRegistration(); } - if (canManageSharedRoomSettings && nextChatType) { - const nextRoomContexts = - shouldPersistRoomDefaultContextIds || shouldPersistRoomCustomContext - ? upsertChatRoomContextSettings(roomContexts, { - sessionId: snapshot.conversation.sessionId, - defaultContextIds: normalizedDefaultContextIds, - customContextTitle: nextCustomContextTitle, - customContextContent: nextCustomContextContent, - }) - : roomContexts.filter((item) => item.sessionId !== snapshot.conversation.sessionId); + if (shouldSaveRoomContextSettings) { + const shouldKeepRoomContextRecord = + shouldPersistRoomDefaultContextIds + || shouldPersistRoomCustomContext + || currentRoomCodexParticipants.length > 0; + const nextRoomContexts = shouldKeepRoomContextRecord + ? upsertChatRoomContextSettings(roomContexts, { + sessionId: snapshot.conversation.sessionId, + defaultContextIds: normalizedDefaultContextIds, + customContextTitle: nextCustomContextTitle, + customContextContent: nextCustomContextContent, + codexParticipants: currentRoomCodexParticipants, + }) + : roomContexts.filter((item) => item.sessionId !== snapshot.conversation.sessionId); await setChatContextSettingsStore({ defaultContexts, chatTypeDefaults, roomContexts: nextRoomContexts, }); - } const shouldSaveAccessPinSettings = @@ -3703,6 +3717,10 @@ export function ChatSharePage() { message, normalizedToken, roomNotificationClientStatus.tone, + activeRoomContextSettings?.codexParticipants, + activeRoomContextSettings?.customContextContent, + activeRoomContextSettings?.customContextTitle, + activeRoomContextSettings?.defaultContextIds, roomContexts, setChatContextSettingsStore, snapshot?.conversation.notifyOffline,