fix(clock): preserve UTC leases and unification recovery

This commit is contained in:
2026-09-03 19:40:14 +00:00
parent 62006fb2d4
commit a2b0b997a2
9 changed files with 351 additions and 21 deletions
@@ -222,6 +222,14 @@ export const planProfileReconcile = (
};
};
/**
* A stopped process may be restarted while the world remains in unification
* wait. Only the daemon-authorized raise-invader response may reconcile that
* suspension, so Gateway RESUME must start the runtime without consuming it.
*/
export const shouldStartRuntimeInUnificationWait = (clockPhase: string, suspensionSource: string): boolean =>
clockPhase === 'SUSPENDED' && suspensionSource === 'UNIFICATION_WAIT';
export const resolveResetLifecycleStatus = (
now: Date,
preopenAt: Date | null,
@@ -1311,6 +1319,9 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
orderBy: { createdAt: 'desc' },
});
if (!suspension) throw new Error('Profile resume requires a durable suspended clock ledger.');
if (shouldStartRuntimeInUnificationWait(world.clockPhase, suspension.source)) {
return { phase: 'SUSPENDED', revision: clockRevisionAsNumber(world.clockRevision) };
}
if (world.clockPhase === 'SUSPENDED') {
await reconcileClockSuspension({ db: postgres.prisma, suspensionId: suspension.id, authority });
} else if (world.clockPhase !== 'RECONCILING') {
+18 -1
View File
@@ -13,6 +13,7 @@ import {
planProfileReconcile,
resolveProfileArchiveServerName,
resolveResetLifecycleStatus,
shouldStartRuntimeInUnificationWait,
} from '../src/orchestrator/gatewayOrchestrator.js';
import { GATEWAY_PROFILE_ORDER } from '../src/profileOrder.js';
import { sanitizeManagedProcessEnv } from '../src/orchestrator/processManager.js';
@@ -114,6 +115,20 @@ describe('planProfileReconcile', () => {
});
});
describe('shouldStartRuntimeInUnificationWait', () => {
it('starts a stopped runtime without consuming its daemon-authorized unification suspension', () => {
expect(shouldStartRuntimeInUnificationWait('SUSPENDED', 'UNIFICATION_WAIT')).toBe(true);
});
it.each([
['SUSPENDED', 'MAINTENANCE'],
['RECONCILING', 'UNIFICATION_WAIT'],
['RUNNING', 'UNIFICATION_WAIT'],
])('keeps ordinary resume reconciliation for %s / %s', (phase, source) => {
expect(shouldStartRuntimeInUnificationWait(phase, source)).toBe(false);
});
});
describe('resolveResetLifecycleStatus', () => {
const now = new Date('2030-01-01T00:00:00.000Z');
@@ -471,7 +486,9 @@ describe('buildWorkspaceCommands', () => {
);
const migrationUrl = new URL(command.env?.DATABASE_URL ?? '');
expect(migrationUrl.searchParams.getAll('options')).toEqual(['-c statement_timeout=30000 -c TimeZone=Asia/Seoul']);
expect(migrationUrl.searchParams.getAll('options')).toEqual([
'-c statement_timeout=30000 -c TimeZone=Asia/Seoul',
]);
});
it('keeps an already explicit KST migration contract without adding another override', () => {